常见的排序算法汇总(详解篇)

目录

排序的概念以及运用

排序的概念

[1. 插入排序](#1. 插入排序)

[1.1 直接插入排序](#1.1 直接插入排序)

[1.1.1 基本思想](#1.1.1 基本思想)

[1.1.2 代码实现](#1.1.2 代码实现)

直接插入排序的特征总结:

[1.1.3 希尔排序(缩小增量排序)🚀](#1.1.3 希尔排序(缩小增量排序)🚀)

1.1.4基本思想🚀

[1.1.5 代码实现🚀](#1.1.5 代码实现🚀)

希尔排序的特征总结:

2.选择排序

2.1直接选择排序

[2.1.1 基本思想](#2.1.1 基本思想)

[2.1.2 代码实现](#2.1.2 代码实现)

直接选择排序的特征总结:

[2.1.3 堆排序🚀](#2.1.3 堆排序🚀)

2.1.4基本思想🚀

[2.1.5 代码实现🚀](#2.1.5 代码实现🚀)

直接选择排序的特征总结:

3.交换排序

3.1冒泡排序

3.1.1基本思想

3.1.2代码实现

直接选择排序的特征总结:

[3.1.3 快速排序(重点)🚀(未完成)](#3.1.3 快速排序(重点)🚀(未完成))

[3.1.4 实现思路🚀](#3.1.4 实现思路🚀)

[->1. 快速排序第一种实现思路(Hoare版本)🚀](#->1. 快速排序第一种实现思路(Hoare版本)🚀)

方法一完整代码实现(Hoare版本)🚀:

[->2. 第二种实现思路(挖坑法)🚀](#->2. 第二种实现思路(挖坑法)🚀)

​编辑

方法二完整代码实现🚀:

->3.第三种实现方法(前后指针法)🚀

方法三完整代码实现🚀:

[3.1.5 优化快速排序🚀](#3.1.5 优化快速排序🚀)

[3.1.6 快速排序的非递归现实🚀](#3.1.6 快速排序的非递归现实🚀)

3.1.7快速排序三路划分🚀(解决重复数据)

基本思想🚀:

代码实现🚀:

快速排序的特征总结:

4.归并排序🚀

[4.1 归并排序(递归实现)🚀](#4.1 归并排序(递归实现)🚀)

4.1.1基本思想🚀

[4.1.2 代码实现(递归实现):🚀](#4.1.2 代码实现(递归实现):🚀)

[4.2 归并排序(非递归实现)🚀](#4.2 归并排序(非递归实现)🚀)

代码实现1(一次性拷贝进a数组):🚀

代码实现2(归并一部分就拷贝一部分进数组a):🚀

归并排序的特征总结:

5.非比较排序

5.1计数排序

5.1.1基本思路

[5.1.2 计数排序代码实现:](#5.1.2 计数排序代码实现:)

计数排序的特征总结:

6.排序算法复杂度及稳定性分析

7.各个算法效率比较


排序的概念以及运用

排序的概念

  • **排序 :**所谓排序,就是使一串记录,按照其中的某个或某些关键字的大小,递增或递减的排列起来的操作。
  • **稳定性 :**假定在待排序的记录序列中,存在多个具有相同的关键字的记录,若经过排序,这些记录的相对次序保持不变,即在原序列中, r[i]=r[j] ,且 r[i] 在 r[j] 之前,而在排序后的序列中, r[i] 仍在 r[j] 之前,则称这种排序算法是稳定的;否则称为不稳定的。
  • 例:
  • **内部排序 :**数据元素全部放在内存中的排序。
  • **外部排序 :**数据元素太多不能同时放在内存中,根据排序过程的要求不能在内外存之间移动数据的排序。

现在我们讲解的算法如图所示

1. 插入排序

1.1 直接插入排序

  • 当插入第 i(i>=1) 个元素时,前面的 array[0],array[1],...,array[i-1] 已经排好序,此时用 array[i] 的排序码与array[i-1],array[i-2],...的排序码顺序进行比较,找到插入位置即将 array[i] 插入,原来位置上的元素顺序后移

1.1.1 基本思想

直接插入排序是一种简单的插入排序法,其基本思想是:

  • 把待排序的记录按其关键码值的大小逐个插入到一个已经排好序的有序序列中,直到所有的记录插入完为止,得到一个新的有序序列

在实际中我们玩扑克牌的时候就用到了插入排序的思想

1.1.2 代码实现

cpp 复制代码
//直接插入排序,以升序为例
void InsertSort(int* a, int n)
{
	for(int i = 1; i < n; ++i)
	{ 
		int end = i - 1;
		int tmp = a[i];

		while (end >= 0)
		{
			//将小于tmp的值往后面挪动
			if (tmp < a[end])
			{
				a[end + 1] = a[end];
				--end;
			}
			else
			{
				//不是每次end小于0的情况
				break;
			}
		}
		//将tmp放到,需要插入的位置
		a[end + 1] = tmp;
	}
}

直接插入排序的特征总结:

1.元素集合越接近有序,直接插入排序算法的时间效率越高

2.时间复杂度

最坏情况:O(N^2)

最好情况:O(N) (就算这组数据是有序的它也得遍历N遍)

时间复杂度一般取最坏情况:O(N^2)

3.空间复杂度:O(1) ,它是一种稳定的排序算法

4.稳定性:稳定

1.1.3 希尔排序(缩小增量排序)🚀

1.1.4基本思想🚀

**希尔排序是直接插入排序的改良版,希尔排序又称:缩小增量法。**希尔排序的基本思想是:

  • 先选定一个整数,把待排序的文件中的所有记录分成组,所有距离相同的记录分在同一组内,并对每一组内的记录进行排序。然后,重复上述分组和排序的工作。当距离达到1时,所有距离在统一组内排好序

1.1.5 代码实现🚀

cpp 复制代码
//希尔排序
void ShellSort(int* a, int n)
{
    int gap = n;

    while(gap > 1) //这里循环条件不能等于1,不然会死循环
    {
        gap = gap / 3 + 1; //这里使用gap /= 2;也可以

        for(int i = 0; i < n - gap; ++i)
        {
            int end = i;
            int tmp = a[i + gap];
            
            while(end >= 0)
            {
                if(tmp < a[end])
                {
                    a[end + gap] = a[end];
                    end -= gap;
                }
                else
                {
                    break;
                }
            }
            a[end + gap] = tmp;
        }
    }    
    
}

希尔排序的特征总结:

1.希尔排序是对直接插入排序的优化

2.当gap > 1时都是预排序,目的是让数组更接近于有序。当gap == 1时(我们的时间复杂度基就是最好情况:O(N),如果没有进行预排这里就按最坏情况来算),数组已经接近有序了,这样就会很快。这样整体而言,可以达到优化的效果

3.希尔排序的时间复杂度不好计算,因为gap的取值方法有很多,导致很难去计算,因此在好些树中给出的希尔排序的时间复杂度都不固定

所以这里直接给出结论:希尔排序的时间复杂度就按照:**O(N^1.3)**来记就可以了

4.稳定性:不稳定

2.选择排序

2.1直接选择排序

  • 在元素集合 array[i]--array[n-1] 中选择关键码最大 ( 小 ) 的数据元素
  • 若它不是这组元素中的最后一个 ( 第一个 ) 元素,则将它与这组元素中的最后一个(第一个)元素交换
  • 在剩余的array[i]--array[n-2] ( array[i+1]--array[n-1] )集合中,重复上述步骤,直到集合剩余 1 个元素

2.1.1 基本思想

每一次从待排序的数据元素中选出最小(或最大)的一个元素,存放在序列的起始位置,直到全部待排序的数据元素排完

2.1.2 代码实现

cpp 复制代码
//交换
void Swap(int* x, int* y)
{
    int tmp = *x;
    *x = *y;
    *y = tmp;
}

//直接选择排序
void SelectSort(int* a, int n)
{
	int left = 0;
	int right = n - 1;

	int mini, maxi;

	//随便给初值
	while (left < right)
	{
		//每次循环都需要重置mini,maxi,因为是上一次的结果不计入这次循环。
		mini = left, maxi = left; 

		for (int i = left + 1; i <= right; ++i)
		{
			if (a[i] > a[maxi])
			{
				maxi = i;
			}

			if (a[i] < a[mini])
			{
				mini = i;
			}
		}

		Swap(&a[left], &a[mini]);

		//如果left于maxi重叠

		if (left == maxi)
		{
			maxi = mini;
		}

		Swap(&a[right], &a[maxi]);

		left++;
		right--;
	}
}

直接选择排序的特征总结:

1.直接选择排序思考非常好理解,但是效率不是很好。实际中很少使用

2.时间复杂度:

最坏情况:O(N^2)

最好情况:O(N^2)

最好情况也需要将整个数据遍历N^2次,所以这个排序在实际中很少使用

3.空间复杂度:O(1)

4.稳定性:不稳定

2.1.3 堆排序🚀

2.1.4基本思想🚀

总共分为两个步骤

1.建堆

  • 升序:建大堆
  • 降序:建小堆

思路图示:

升序为什么不建小堆?

如果排升序建的是小堆,根结点的数据是最小的,剩下的做堆,选次小的,与上面删除堆尾元素一样,这样做会导致后面父子关系全乱了,得重新排序

如图:

排升序,建大堆第一个元素与最后一个元素交换,然后将最后一个元素抹去(不计入建堆中)其他n - 1个元素建堆,以此类推直到有序即可

2.1.5 代码实现🚀

cpp 复制代码
//向上调整,排升序,建大堆
void AdjustUp(int* a, int child)
{
    assert(a);

    int parent = (child - 1)/2;
    while(child > 0)    
    {
        if(a[parent] < a[child])
        {
            Swap(&a[parent], &a[child]);
            child = parent;
            parent = (child - 1)/2;
        }
        else
        {
            break;
        }
    }
}

//向下调整,建大堆
void AdjustDown(int* a, int n, int parent)
{
    assert(a);

    int child = parent * 2;

    while(child <= n)
    {
        if((child + 1) <= n && a[child + 1] > a[child])
        {
            ++child;
        }

        if(a[parent] < a[child])
        {
            Swap(&a[parent], &a[child]);
            parent = child;
            child = parent * 2;
        }
        else
        {
            break;
        }
    }    
}

//堆排序
void HeapSort(int* a, int n)
{
    int end = n - 1;    

    //向上调整建堆
    /*for(int i = end; i > 0; --i)
    {
        AdjustUp(a,i);
    } */   

    //向下调整建堆
    for(int i = (end - 1)/2; i <= end; --i)
    {
        AdjustDown(a, end + 1, i);
    }

    //排序
    while(end > 0)
    {
        Swap(&a[0], &a[end--]);
        AdjustDown(a, end, 0);
    }
    
}

这里为什么不使用向上建堆?向下排序比向上排序的时间复杂度快一点

向下排序的时间复杂度:O(N)

向上排序的时间复杂度:O(N*logN)

向下排序时间复杂度解析:

向下排序时间复杂度解析:

直接选择排序的特征总结:

1. 堆排序使用堆来排序效率就高了很多

2. 时间复杂度:O(N*logN)

3.空间复杂度:O(1)

4.稳定性:不稳定

3.交换排序

所谓交换,就是根据序列中两个记录键值的比较结果来对换这两个记录在序列中的位置,交换排序的特点是:将键值较大的记录向序列的尾部移动,键值较小的记录向序列的前部移动。

3.1冒泡排序

3.1.1基本思想

冒泡排序也称沉底法:以升序为例,每一次执行都会将最大的数沉底

3.1.2代码实现

cpp 复制代码
//冒泡排序
void BubbleSort(int* a, int n)
{
	for (int i = 0; i < n - 1; ++i)
	{
		bool exchange = false;
		for(int j = 1; j < n - i; ++j)
		{
			if (a[j - 1] > a[j])
			{
				Swap(&a[j - 1], &a[j]);
				exchange = true;
			}
		}

		if (!exchange)
		{
			break;
		}
	}
}

冒泡排序的特征总结:

1.冒泡排序是一种非常容易理解的排序

2.时间复杂度:

最坏情况:O(N^2)

最好情况:O(N)

3.空间复杂度:O(1)

4.稳定性:稳定

3.1.3 快速排序(重点)🚀(未完成)

快速排序是 Hoare 于 1962 年提出的一种二叉树结构的交换排序方法,其基本思想为: 任取待排序元素序列中 的某元素作为基准值,按照该排序码将待排序集合分割成两子序列,左子序列中所有元素均小于基准值,右子序列中所有元素均大于基准值,然后最左右子序列重复该过程,直到所有元素都排列在相应位置上为止 。

3.1.4 实现思路🚀

->1. 快速排序第一种实现思路(Hoare版本)🚀

第一趟排序思路

1.选择关键值做key,这里我们选L做key,R先走,反之L先走

2.R遍历到小于key的位置后停止,L开始遍历,直到大于key的位置停止,然后两个位置的值交换

3.R和L相遇之后将key与相遇位置互换

以下是第一趟排序代码

cpp 复制代码
//快速排序
void QuickSort(int* a, int left, int right)
{
    int end, begin;
    end = left, begin = right;

    int keyi = left;
    while(left < right)
    {
        while(left < right && a[right] >= a[keyi])
                --right;

        while(left < right && a[left] <= a[keyi])
                ++left;

        Swap(&a[left], &a[right]);
    }

    Swap(&a[left], &a[keyi]);
}

完成以上步骤之后:

所有大于key的值都在key的左边,小于key的值都在key的右边

然后我们将这组数据分为三个区间:

然后我们将除了key之外的两个区间进行与上面相同的步骤

使用递归的方法(如图):

如图所示:当递归结束之后,这组数据就有序了

方法一完整代码实现(Hoare版本)🚀:
cpp 复制代码
//快速排序
int QuickSort1(int* a, int left, int right)
{
    int keyi = left;
    while(left < right)
    {
        while(left < right && a[right] >= a[keyi])
                --right;

        while(left < right && a[left] <= a[keyi])
                ++left;

        Swap(&a[left], &a[right]);
    }

    Swap(&a[left], &a[keyi]);
    keyi = left;

    return keyi;
}

void test(int* a, int left, int right)
{
    //递归结束的条件
    if(left > right)
    {
        return;
    }

    int keyi = QuickSort1(a, left, right);
    test(a, left, keyi - 1);
    test(a, keyi + 1, right);
}

关于关键值key,这里还有一些优化

->1.随机取key

cpp 复制代码
//随机取key
int randi = left + (rand() % (right - left));
Swap(&a[randi], &a[left]);
int keyi = left;

->2.三数取中(取三个数中第二大的那个值的下标)

cpp 复制代码
//三数取中
int GetMidNum(int* a, int left, int right)
{
    assert(a);
    
    int mid = left + (right - left)/2;

    if(a[mid] > a[left])
    {
        if(a[mid] < a[right])
        {
            return mid;
        }
        else if(a[left] > a[right])
        {
            return left;
        }
        else
        {
            return right;
        }
    }
    else
    {
        if(a[mid] > a[right])
        {
            return mid;
        }
        else if(a[left] < a[right])
        {
            return left;
        }
        else
        {
            return right;
        }
    }
}
cpp 复制代码
//三数取中
int midi = GetMidNum(a, left, right);
Swap(&a[midi], a[left]);
int keyi = left;

一直往下递归,每递归一次就会少一个key(少一个数),该递归形状与二叉树相似,所以高度可以看作:logn

虽然N一直在减(减的很少),但是递归到最后,其实N还是在N这个量级里面,将它看作N就可以了

所以快速排序的时间复杂度大致可以看作为:O(N*logN)

这两个优化有什么用呢?

1.如果key固定在R或者L上取值的话,当数组有序时会变得很麻烦,甚至栈溢出

一直往下递归,每次递归只有一个key,所以当数据很大时候很容易造成栈溢出

2.我们一般使用的是三数取中,因为这个找key比较科学

->2. 第二种实现思路(挖坑法)🚀

大家看图体会一下

挖坑法比第一种方法要简介一点

我这里直接给代码了

方法二完整代码实现🚀
cpp 复制代码
//快速排序挖坑法
int QuickSort2(int* a, int left, int right)
{
	//三数取中
	int midi = GetMidNum(a, left, right);
    if(midi != left)
	Swap(&a[left], &a[midi]);

	//这里必须存left这个为位置的值,来保存这个位置的值
	int key = a[left];
	int hole = left;

	while (left < right)
	{
		while (left < right && a[right] >= key)
			--right;

		a[hole] = a[right];
		hole = right;

		while (left < right && a[left] <= key)
			++left;

		a[hole] = a[left];
		hole = left;
	}
	a[hole] = key;
    
    return hole;
}

void test(int* a, int left, int right)
{
    //递归结束的条件
    if(left > right)
    {
        return;
    }

    int keyi = QuickSort2(a, left, right);
    test(a, left, keyi - 1);
    test(a, keyi + 1, right);
}
->3.第三种实现方法(前后指针法)🚀

这个方法是必须掌握的,前后指针法比上面两种代码量都要少

实现思路是:以上图为例

cur找到比key值小的后,++prev,然后prev与cur位置的值交换,cur++,当cur遍历完数组之后结束

方法三完整代码实现🚀
cpp 复制代码
//前后指针法
int QuickSort3(int* a, int left, int right)
{
    //三数取中
    int midi = GetMidNum(a, left, right);
    if(midi != left)
    Swap(&a[midi], &a[left]);
    int keyi = left;

    int prev = left;
    int cur = left + 1;
    
    while(cur <= right)
    {
        if(a[cur] < key && ++prev != cur)
            Swap(&a[cur], &a[prev]);

        ++cur;
    }    
    
    Swap(&a[prev], &a[keyi]);
    keyi = prev;
}

void test(int* a, int left, int right)
{
    if(left > right)
    {
        return;
    }
    
    int keyi = QuickSort3(a, left, right);
    test(a, left, keyi - 1);
    test(a, keyi + 1, right);
}

3.1.5 优化快速排序🚀

如果后面的递归,我们自己来排,不使用递归,那快速排序是不是会快?

我们来试一下

cpp 复制代码
void test(int* a, int left, int right)
{
    if(left > right)
    {
        return;
    }
    
    //小区间优化--小区间直接使用插入排序
    if((right - left + 1) > 10)
    {
        int keyi = QuickSort(a, left, right);
        test(a, left, keyi - 1);
        test(a, keyi + 1, right);
    }
    else
    {
        InsertSort(a + left, right - left + 1);
    }
}

3.1.6 快速排序的非递归现实🚀

这里我们使用深度优先遍历,利用栈来实现快速排序的非递归

这里不知道栈的,可以看我上几篇博客 跳转栈

cpp 复制代码
//非递归方法,使用栈实现
void test(int* a, int left, int right)
{
    ST st;
    STInit(&st);
    STPush(&st, right);
    STPush(&st, left);

    while(!STEmpty(&st))
    {
        int begin = STTop(&st);
        STPop(&st);
        int end = STTop(&st);
        STPop(&st);
        
        int keyi = QuickSort3(a, begin, end);
        
        if(begin < keyi - 1)
        {
            STPush(&st, Keyi - 1);
            STPush(&st, begin);
        }
        if(end > keyi + 1)
        {
            StPush(&st, end);
            STPush(&st, keyi + 1);
        }
    }
}

3.1.7快速排序三路划分🚀(解决重复数据)

对于有很多数值相等的排序任务来说,将数值想相等的元素挤到中间,每一次确定位置的元素变多了递归处理的区间长度也减少了,总的来说,三路划分是应对相等元素较多的排序任务

注:lt:less than gt:greater than

基本思想🚀:

将小于V的值放在第一个区间( i 位置的元素和 第一个区间最后一个元素的后一个位置交换),等于V的放在第二个区间(i 继续往后走,不用处理) 大于V的放在第三个区间(将 i位置上的元素与第三区间上的第一个元素的前一个交换),直至i > gt结束,最后将key与第一个区间最后一个元素交换位置的值,使key到第二个区间。i遍历整个数组

代码实现🚀:
cpp 复制代码
//快速排序三路划分
void QuickSortThreeWay(int* a, int left, int right)
{
	if (left >= right)
		return;

	int begin = left, end = right;
	//三数取中
	int midi = GetMidNum(a, left, right);
    if(midi != left)
	Swap(&a[midi], &a[left]);
	int keyi = left;

	int lt = left, gt = right;
	int i = left + 1;

    //分区间
	while (i <= gt)
	{
		if (a[i] < a[keyi] && ++lt != i)
		{
			Swap(&a[lt], &a[i]);
			++i;
		}
		else if (a[i] == a[keyi])
		{
			++i;
		}
		else if (a[i] > a[keyi])
		{
			Swap(&a[gt], &a[i]);
			--gt;
		}
		else
		{
			++i;
			continue;
		}
	}

	Swap(&a[lt], &a[keyi]);
	
    //对另外两个区间递归排序
	QuickSortThreeWay(a, begin, lt - 1);
	QuickSortThreeWay(a, gt + 1, end);
}

快速排序的特征总结:

1.快速排序整体的综合性能和使用场景都是比较好的,所以才敢叫:快速排序

2.时间复杂度:O(N*logN)

3.空间复杂度:O(logN)

4.稳定性:不稳定

4.归并排序🚀

4.1 归并排序(递归实现)🚀

4.1.1基本思想🚀

归并排序( MERGE-SORT )是建立在归并操作上的一种有效的排序算法 , 该算法是采用分治法( Divide andConquer)的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为二路归并。 归并排序核心步骤:

4.1.2 代码实现(递归实现):🚀

cpp 复制代码
//归并排序
void _MergeSort(int* a, int left, int right, int* tmp)
{
	if (left >= right)
		return;

	int mid = left + (right - left) / 2;

	_MergeSort(a, left, mid, tmp);
	_MergeSort(a, mid + 1, right, tmp);

	int i = left;
	int begin1 = left, end1 = mid;
	int begin2 = mid + 1, end2 = right;

	while (begin1 <= end1 && begin2 <= end2)
	{
		if (a[begin1] <= a[begin2])
		{
			tmp[i++] = a[begin1++];
		}
		else 
		{
			tmp[i++] = a[begin2++];
		}
	}

	while (begin1 <= end1)
	{
		tmp[i++] = a[begin1++];
	}

	while (begin2 <= end2)
	{
		tmp[i++] = a[begin2++];
	}

	memcpy(a + left, tmp + left , sizeof(int) * (right - left + 1));
}

void MergeSort(int* a, int left, int right)
{
	int* tmp = (int*)malloc(sizeof(int) * (right - left + 1));
	if (NULL == tmp)
	{
		perror("MergeSort::malloc");
		return;
	}

	_MergeSort(a, left, right, tmp);

	free(tmp);
}

方便大家理解我这里画一个递归展开图:

4.2 归并排序(非递归实现)🚀

代码实现1(一次性拷贝进a数组):🚀

cpp 复制代码
//归并排序(非递归)
void MergeSortNonR(int* a, int n)
{
	int* tmp = (int*)malloc(sizeof(int) * n);
	if (NULL == tmp)
	{
		perror("MergeSort::malloc");
		return;
	}

	int gap = 1;
	while (gap < n)
	{
		for (int i = 0; i < n; i +=2 * gap)  //这里的i +=2 * gap是跳跃已经排完的区间
		{
			int begin1 = i, end1 = i + gap - 1;
			int begin2 = i + gap, end2 = i + gap * 2 - 1;
			int j = i;
			printf("[%d][%d],[%d][%d] ", begin1,end1,begin2,end2);
			while (begin1 <= end1 && begin2 <= end2)
			{
				if (a[begin1] <= a[begin2]) //这里加上等于号,归并排序就变的稳定了
				{
					tmp[j++] = a[begin1++];
				}
				else
				{
					tmp[j++] = a[begin2++];
				}
			}

			while (begin1 <= end1)
			{
				tmp[j++] = a[begin1++];
			}

			while (begin2 <= end2)
			{
				tmp[j++] = a[begin2++];
			}
		}
		gap *= 2;
        //一次拷贝进a数组,需要放在循环中,每次都给改变a数组的值
	    memcpy(a, tmp, sizeof(int) * n);
		printf("\n");
	}

	free(tmp);
	tmp = NULL;
}

测试运行:

测试用例:

出现这个情况不要着急,一般是细节没做好导致越界

我将它分割区间的过程打印出来了

我们只有10个元素下标0~9,但是这里我用红色圈出来的都比9要大很明显这里区间范围取大了

这里我们将复杂问题分解为简单问题:分类处理

1. 因为begin1等于i,是绝对不可能越界的,当end1越界时,这时候就不归并了

2.end1没有越界,begin2越界了,end2肯定也越界了,跟begin1一样处理,但是要 将它们改为不存在区间

如图:

3.begin1,begin2没有越界,end2越界了,修正end2,继续归并

然后tmp将数据放入a数组中

以上就是一次性拷贝进a数组的坏处

修正之后代码:

cpp 复制代码
//归并排序(非递归)
void MergeSortNonR(int* a, int n)
{
	int* tmp = (int*)malloc(sizeof(int) * n);
	if (NULL == tmp)
	{
		perror("MergeSort::malloc");
		return;
	}

	int gap = 1;
	while (gap < n)
	{
		for (int i = 0; i < n; i += 2 * gap)
		{
			int begin1 = i, end1 = i + gap - 1;
			int begin2 = i + gap, end2 = i + gap * 2 - 1;
			int j = i;
            
            //修正代码
			if (end1 >= n)
			{
				end1 = n - 1;
				begin2 = n;
				end2 = n - 1;
			}
			else if (begin2 >= n)
			{
				begin2 = n;
				end2 = n - 1;
			}
			else if (end2 >= n)
			{
				end2 = n - 1;
			}


			printf("[%d][%d],[%d][%d] ", begin1,end1,begin2,end2);
			while (begin1 <= end1 && begin2 <= end2)
			{
				if (a[begin1] <= a[begin2]) //这里加上等于号,归并排序就变的稳定了
				{
					tmp[j++] = a[begin1++];
				}
				else
				{
					tmp[j++] = a[begin2++];
				}
			}

			while (begin1 <= end1)
			{
				tmp[j++] = a[begin1++];
			}

			while (begin2 <= end2)
			{
				tmp[j++] = a[begin2++];
			}
		}
		gap *= 2;
		//一次拷贝进a数组
		memcpy(a, tmp, sizeof(int) * n);
		printf("\n");
	}
	free(tmp);
	tmp = NULL;
}

运行测试:

修正之前与修正之后对比:

对比可知,我们已经将越界部分都处理掉了

代码实现2(归并一部分就拷贝一部分进数组a):🚀

cpp 复制代码
void MegerSortNonR(int* a, int n)
{
    int* tmp = (int*)malloc(sizeof(int) * n);
    if(NULL == tmp)
    {
        perror("MegerSortNonR::malloc");
        return;
    }

    int gap = 1;
    while(gap < n)
    {
        for(int i = 0; i < n; i += 2*gap)
        {
            int begin1 = i, end1 = i + gap - 1;
            int begin2 = i + gap, end2 = i + 2 * gap - 1;
            int j = i;
            
            //修正
            if(end1 <= n && begin2 >= n)
            {
                break;
            }
            else if(end2 >= n)
            {
                end2 = n - 1;
            }

            while(begin1 <= end1 && begin2 <= end2)
            {
                if(a[begin1] <= a[begin2])
                {
                    tmp[j++] = a[begin1++];
                }
                else
                {
                    tmp[j++] = a[begin2++];
                }
            }
            
            while(begin1 <= end1)
            {
                tmp[j++] = a[begin1++];
            }
            while(begin2 <= end2)
            {
                tmp[j++] = a[begin2++];
            }
            //a和tmp都得加i,因为a不加i的话,tmp + i就会覆盖a数组中前面的元素,这是memcpy的功能
            memcpy(a + i, tmp + i, sizeof(int) * (end2 - i + 1));
        }
        gap *= 2;
    }
    
    free(tmp);
    tmp = NULL;
}

如果是这样的话,对于区间的修正就简单多了

  1. end1,begin2越界, 直接break就可以了,因为我们是归并一次就拷一次所以不用考虑是否会缺数据,或者覆盖的问题

  2. end2越界,修正一下,继续归并

归并排序的特征总结:

1.归并的缺点在于需要O(N)的空间复杂度,归并排序的思考更多的是解决在磁盘中的外排序问题。

2.时间复杂度:O(N*logN)

3.空间复杂度:O(N)

4.稳定性:稳定

5.非比较排序

计数排序,基数排序,桶排序这三种排序都是非比较排序,因为后两种在实际中很少使用,也没什么价值,所以这里我们说明一下计数排序

5.1计数排序

计数排序又叫鸽巢原理,只适合范围集中,且范围不大的整形数组排序。

不适合范围分散或者非整形的排序,如:字符串,浮点数等

5.1.1基本思路

相对位置排序,只要是范围就紧凑的整数数据(负整数也算),就好用

5.1.2 计数排序代码实现:

cpp 复制代码
//计数排序
void CountSort(int* a, int n)
{
	int max, min;
	max = a[0], min = a[0];
	for (int i = 0; i < n; ++i)
	{
		if (a[i] > max)
		{
			max = a[i];
		}

		if (a[i] < min)
		{
			min = a[i];
		}
	}
	int range = max - min + 1;
	int* CountA = (int*)calloc(range, sizeof(int));
	if (CountA == NULL)
	{
		perror("CountA::malloc");
		return;
	}

	for (int i = 0; i < range; ++i)
	{
		CountA[a[i] - min]++;
	}

	//排序
	int j = 0;
	for (int i = 0; i < range; ++i)
	{
		//因为是计数,所以使用循环
		while (CountA[i]--)
		{
			//下标加a数组最小值
			a[j++] = i + min;
		}
	}

	free(CountA);
	CountA = NULL;
}

计数排序的特征总结:

1.计数排序在数据范围集中时,效率很高,但是适用范围有限及场景有限

2.时间复杂度:O(MAX(N, 范围))

3.空间复杂度:O(范围)

4.稳定性:稳定

6.排序算法复杂度及稳定性分析

7.各个算法效率比较

一万数据排序

单位:毫秒

一千万数据排序,选择排序,插入排序,冒泡排序,就不用计算了,这组数据比较希尔排序,堆排序,快速排序,归并排序

以上就是全部内容了,数据结构初阶终于结束了!!!

相关推荐
一只码代码的章鱼24 分钟前
排序算法 (插入,选择,冒泡,希尔,快速,归并,堆排序)
数据结构·算法·排序算法
青い月の魔女43 分钟前
数据结构初阶---二叉树
c语言·数据结构·笔记·学习·算法
我要出家当道士1 小时前
Nginx单向链表 ngx_list_t
数据结构·nginx·链表·c
林的快手1 小时前
209.长度最小的子数组
java·数据结构·数据库·python·算法·leetcode
千天夜2 小时前
多源多点路径规划:基于启发式动态生成树算法的实现
算法·机器学习·动态规划
从以前2 小时前
准备考试:解决大学入学考试问题
数据结构·python·算法
.Vcoistnt2 小时前
Codeforces Round 994 (Div. 2)(A-D)
数据结构·c++·算法·贪心算法·动态规划
ALISHENGYA2 小时前
全国青少年信息学奥林匹克竞赛(信奥赛)备考实战之分支结构(实战训练三)
数据结构·c++·算法·图论
我码玄黄5 小时前
正则表达式优化之算法和效率优化
前端·javascript·算法·正则表达式
Solitudefire5 小时前
蓝桥杯刷题——day9
算法·蓝桥杯