八大排序详解

目录

1.排序的概念及应用

1.1 排序的概念

1.2 排序的应用

1.3 常见的排序算法

2.常见排序算法的实现

2.1 直接插入排序

2.1.1 基本思想

2.1.2 动图解析

2.1.3 排序步骤(默认升序)

2.1.4 代码实现

2.1.5 特性总结

2.2 希尔排序

2.2.1 基本思想

2.2.2 排序步骤

2.2.3 代码实现

2.2.4 动图解析

2.2.5 特性总结

2.3 选择排序

2.3.1 基本思想

2.3.2 排序步骤(升序)

2.3.3 代码实现

2.3.4 动图解析

2.3.5 特性总结

2.4 堆排序

2.5 冒泡排序

2.5.1 基本思想

2.5.2 代码实现

2.5.3 动图解析

2.5.4 特性总结

2.6 快速排序

2.6.1 基本思想

2.6.2 排序步骤

2.6.3 代码实现

区间划分算法(hoare初始版本):

主框架:

2.6.4 区间划分算法

hoare法

挖坑法

前后指针法

2.6.5 快排优化

取key方面的优化

递归方面的优化

2.6.6 快排非递归实现

栈实现(代码+图解):

队列实现:

2.6.7 特性总结

2.7 归并排序​

2.7.1 基本思想

2.7.2 排序步骤

2.7.3 代码实现

2.7.4 动图解析

2.7.5 非递归实现 

2.7.6 特性总结

2.8计数排序

2.8.1 基本思想

2.8.2 排序步骤

2.8.3 代码实现

2.8.4 特性分析

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


1.排序的概念及应用


1.1 排序的概念

排序:所谓排序,就是使一串记录,按照其中的某个或某些关键字的大小,递增或递减的排列起来的操作。

稳定性:假定在待排序的记录序列中,存在多个具有相同的关键字的记录,若经过排序,这些记录的相对次序保持不变,即在原序列中,r[i]=r[j],且r[i]在r[j]之前,而在排序后的序列中,r[i]仍在r[j]之前,则称这种排序算法是稳定的;否则称为不稳定的。

内部排序:数据元素全部放在内存中的排序。 外部排序:数据元素太多不能同时放在内存中,根据排序过程的要求不能在内外存之间移动数据的排序。

1.2 排序的应用

排序算法在计算机科学和实际应用中有广泛的应用。以下是一些常见的排序算法的应用示例:

  1. 数据库查询:在数据库中,经常需要根据某个字段对数据进行排序,以便更快地进行查询和检索。排序算法可以用于对数据库中的数据进行排序,从而提高查询效率。

  2. 搜索算法:许多搜索算法的实现需要对数据进行排序。例如,在二分查找算法中,要求待搜索的数据必须是有序的。因此,使用排序算法对数据进行排序,可以提高搜索算法的效率。

  3. 负载均衡:在分布式系统中,负载均衡是一种优化策略,通过将工作负载均匀地分配给各个节点,以提高系统的性能和吞量。排序算法可以用于对请求或任务进行排序,以便将工作负载均匀地分布给各个节点。

  4. 数据压缩:在数据压缩算法中,排序算法可以用于对数据进行预处理,以便更好地利用压缩算法的特性。例如,在哈夫曼编码中,可以根据字符频率对字符进行排序,以便构建最优的编码树。

  5. 排序和统计:在统计分析中,需要对数据进行排序,以便进行数据的聚合、分组和分析。排序算法可以用于对数据进行排序,从而更方便地进行统计分析。

  6. 任务调度:在任务调度算法中,排序算法可以用于对任务进行排序,以便根据任务的优先级、截止时间或其他标准进行合理的任务调度和分配。

总之,排序算法在各个领域中都有广泛的应用。通过对数据进行排序,可以提高系统的性能、搜索算法的效率,以及优化各种应用场景中的数据处理和分析。

1.3 常见的排序算法

2.常见排序算法的实现


2.1 直接插入排序

2.1.1 基本思想

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

直接插入排序希尔排序都属于插入排序。

我们在实际生活中的摸扑克牌用的就是这个思想。

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

2.1.2 动图解析

要点:

  1. 待插入数组已经排好序的。

  2. 待插入数据依次与数组中的元素比较,找到合适的位置。

  3. 该位置及其后面的元素全部后移,待插入数据插入该位置。

  4. 数组中除了第一个元素array[0](默认有序),所有元素都看做待插入数据依次插入数组中,完成排序。

2.1.3 排序步骤(默认升序)

  1. 从第一个元素开始,该元素可以被认为已经有序。

  2. 取下一个元素tmp(待插入元素),从已排序好的数组(待插入数组)中从后往前扫描。

  3. 如果tmp小于该元素,该元素向后挪动一位。

  4. 重复步骤3,直到tmp大于等于已排序数组中的某个元素。

  5. tmp插入到该元素的后面,如果tmp小于该数组中的所有元素,则tmp插入到下标为0的位置。

  6. 重复2~5。

2.1.4 代码实现

我们先写单趟插入:

void Insert(int* a, int n)
{//一次插入int end = 0;int tmp = a[end + 1];while (end >= 0){if (tmp < a[end]){a[end + 1] = a[end];}else{break;}
​end--;}
​a[end + 1] = tmp;
}

代码解析:

  1. end变量:已排序好的数组中最后一个元素的下标,第一次插入从0开始(数组中只有一个元素array[0])

  2. tmp变量:待插入数据,防止被a[end]覆盖,单独用一个变量保存起来。

  3. tmp依次和数组中的元素比较,如果tmp < a[end]则a[end]向后移,如果tmp >= a[end]则找到了插入位置,break退出循环。

整个数组插入(最终版本):

void Insert(int* a, int n)
{//一组插入for(int i = 0; i < n - 1; i++){//i用于控制end(end:待插入数组中最后一个元素的下标)int end = i;int tmp = a[end + 1];while (end >= 0){if (tmp < a[end]){a[end + 1] = a[end];}else{break;}
​end--;}a[end + 1] = tmp;}
}

用一个循环控制end即可完成整个数组的插入,初始end == 0(数组中一个元素array[0]),末尾end == n - 2(数组中有n - 1个元素)。

2.1.5 特性总结

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

2.时间复杂度:

最坏情况(假设升序):数组逆序,时间复杂度O(N^2)

最好情况:数组升序,时间复杂度O(N)

综合来看,时间复杂度O(N^2)

我们可以看出:元素集合越接近有序,直接插入排序算法的时间效率越高。

2.2 希尔排序

前面我们分析得出:元素集合越接近有序,直接插入排序的时间效率越高,那么我们是不是可以利用某种算法让数组不断接近有序,然后再进行直接插入排序?这里就要介绍希尔排序了。


2.2.1 基本思想

希尔排序法又称缩小增量法。希尔排序法的基本思想是:将待排记录序列分割成为若干子序列分别进行插入排序,待整个序列中的记录"基本有序"时,再对全体记录进行一次直接插入排序。

2.2.2 排序步骤

  1. 选定一个整数gap作为第一增量。

  2. 间隔为gap的元素为一组数据,把整个数组分成gap组。

  3. 每一组数据都进行直接插入排序,gap组数据排完之后数组就接近有序。

  4. 取一个比第一增量小的数作为第二增量,重复2~3。

  5. gap == 1时,整个数组为一组进行直接插入排序,完成排序。

  • 间隔为gap,数组就被分成gap组,每组有n/gap个元素。

  • 以gap为间隔进行一趟排序,数组就更加接近有序。

  • gap == 1,就是直接插入排序。

2.2.3 代码实现

  • 间隔为gap的一组数据进行直插排序

void ShellSort(int* a, int n)
{int gap = n / 2;//间隔为gap的一组元素进行直插排序for (int i = 0; i < n - gap; i += gap){int end = i;int tmp = a[end + gap]; while (end >= 0){if (tmp < a[end]) {a[end + gap] = a[end]; }else{break;}
​end -= gap; }
​a[end + gap] = tmp;}
}

这里for循环控制的end变量,应该控制成i < n - gap,因为tmp的下标为end + gap,要保证end + gap < n,所以end < n - gap。或者可以类比直接插入排序,直插的循环控制成i < n - 1 ,gap为1,所以i < n - gap;

  • gap组数据分开排序:

void ShellSort(int* a, int n)
{int gap = n / 2;//gap组数据分开排序for (int j = 0; j < gap; j++){//间隔为gap的一组元素进行直插排序for (int i = j; i < n - gap; i += gap){int end = i;int tmp = a[end + gap];while (end >= 0){if (tmp < a[end]){a[end + gap] = a[end];}else{break;}
​end -= gap;}
​a[end + gap] = tmp;}}
}

这里代码有了三层循环,再加上对gap的缩减就要有四层循环,相对比较复杂,所以我们进行优化:

  • gap组数据并排:

void ShellSort(int* a, int n)
{int gap = n / 2;//gap组数据并排for (int i = 0; i < n - gap; i++){int end = i;int tmp = a[end + gap];while (end >= 0){if (tmp < a[end]){a[end + gap] = a[end];}else{break;}
​end -= gap;}
​a[end + gap] = tmp;}
}

这里的代码的循环变量迭代从 i += gap 变成了 i++,从三层循环优化成了两层循环,但是达到的效果是一样的。

gap组并排和的分开排序不同的是:并排在一组数据还没有插完的情况下又去插入另一组数据,而分开排序是插完一组后才接着插入下一组。

  • 控制gap进行多次直插:(最终版)

void ShellSort(int* a, int n)
{int gap = n;while (gap > 1) {gap = gap / 3 + 1;//gap组数据并排for (int i = 0; i < n - gap; i++) {int end = i; int tmp = a[end + gap]; while (end >= 0) {if (tmp < a[end]) {a[end + gap] = a[end]; }else{break;}
​end -= gap; }
​a[end + gap] = tmp; }}
}

注意这里多gap的控制,要保证最后gap == 1 进行直接插入排序,可以是gap = gap / 2,也可以是gap = gap / 3 + 1;一般使用后者,这样增量缩小的快。

2.2.4 动图解析

2.2.5 特性总结

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

  2. 时间复杂度:

希尔排序的时间复杂度计算相当麻烦,要用到专业的数学知识,官方复杂度为O(N^1.3),比O(N*logN)差一点。

但是我们可以定性分析一下(数组中n个元素):

  • gap很大,比如gap == n / 3:数组分为n / 3组,每组3个数据,每组比较3次,合计(n / 3) * 3 = n次。

  • gap很小,比如gap = = 1:数组接近有序,直接插入,合计n次。

  • gap取中间的值,如n / 9:每组比较1+2+...+8=36次,合计36 * (n / 9) == 4n次。

里面的比较次数是逐渐变化的,两端比较次数少,越往中间比较次数越多。

2.3 选择排序

2.3.1 基本思想

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

选择排序堆排序都属于选择排序。

2.3.2 排序步骤(升序)

  1. 选取一个基准值作为最大值和最小值(默认array[begin])

  2. 遍历array[begin + 1]~array[end]选出最大值max和最小值min

  3. array[begin]和min交换,array[end]和max交换

  4. begin++,end--

  5. 重复1~4

2.3.3 代码实现

步骤相对简单,我们直接写代码

void SelectSort(int* a, int n)
{int begin = 0, end = n - 1;while (begin < end){int minI = begin, maxI = begin; for (int i = begin + 1; i <= end; i++){if (a[i] < a[minI]){minI = i; }if (a[i] > a[maxI]){maxI = i;}}Swap(&a[begin], &a[minI]);
​if (maxI == begin){maxI = minI;}Swap(&a[end], &a[maxI]); begin++;end--;}
}

注意:

Swap(&a[begin], &a[minI])时,下标maxI可能就是begin,max提前被换走,所以就要调整一下maxI的下标。

2.3.4 动图解析

2.3.5 特性总结

  1. 时间复杂度:O(N^2)

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

2.4 堆排序

详见:堆排序

2.5 冒泡排序

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

冒泡排序快速排序都属于交换排序。


2.5.1 基本思想

知名度最高的排序,不多做解释。

如果右边 < 左边,交换左右的值,一直往后交换,每一躺下来最大的数据在右边。

2.5.2 代码实现

void BubbleSort(int* a, int n)
{for (int i = 0; i < n; i++){int exchange = 0; for (int j = 1; j < n - i; j++){if (a[j] < a[j - 1]){Swap(&a[j], &a[j - 1]);exchange = 1;}}//一趟下来没有交换if (exchange == 0)break;}
}

代码优化:

每一趟冒泡排序设置一个exchange变量为0;

如果一趟冒泡发生了交换,exchange置1;

如果一趟冒泡下来exchange还为0,说明没有交换,已经有序,直接break;

2.5.3 动图解析

2.5.4 特性总结

  1. 时间复杂度:O(N^2)

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

2.6 快速排序

2.6.1 基本思想

快速排序采用分治法,任取待排序元素序列中的某元素作为基准值,按照该排序码将待排序集合分割成两子序列,左子序列中所有元素均小于基准值,右子序列中所有元素均大于基准值,然后最左右子序列重复该过程,直到所有元素都排列在相应位置上为止。

2.6.2 排序步骤

  1. 选取基准值,通过区间划分算法把待排序序列分割成左右两部分。

  2. 左右序列中递归重复1。

2.6.3 代码实现

区间划分算法(hoare初始版本):

区间划分算法有三个版本:hoare法,挖坑法,前后指针法,这里介绍hoare法,也是快排的初始划分法。

三种划分方法的结果可能不同,但都是让小的值往前靠拢,大的值往后靠拢,让整个序列逐渐趋于有序。

步骤:

  1. 默认序列最左边的元素为基准值key,设置left,right指针;

  2. left找大,right找小,right要先找,都找到后交换a[left]和a[right];

  3. 重复步骤3

  4. 当left == right时,交换key和相遇位置的元素,完成分割。

走完这一趟后,key值左边都不比key大,key值右边都不比key小,key值到了他排序后应该在的位置,不需要挪动这个元素了。

图解:

算法分析:

为什么能保证相遇位置的值一定比key值小,然后交换?

关键点就是让right先找!

相遇有两种情况:

  • left往right靠拢,与right相遇:right先找到了小的元素,相遇后的值一定比key小。

  • right往left靠拢,与left相遇:left指针指向的元素是上一波交换过后的元素,该元素比key小。

假如我们让left先找的话,相遇位置比key值大,不能交换。

代码:

int Partion(int* a, int left, int right)
{int keyI = left;//left == right两个指针相遇,退出循环while (left < right){//right先找,right找小while (left < right && a[right] >= a[keyI]){right--;}//left找大while (left < right && a[left] <= a[keyI]){left++;}//都找到了,交换Swap(&a[left], &a[right]);}//left和right相遇,交换key和相遇位置元素Swap(&a[keyI], &a[left]);return left;
}

划分方法一般不用hoare,是因为这种算法实现的代码很容易出现bug,比如:

  1. right找小和left找大的过程中,要保证left < right,否则可能出现数组越界,比如1,9,6,4,2,7,8,2 ;右边的值都比key大,会导致越界。

  2. a[right] >= a[keyI]或者a[left] <= a[keyI]时,才能--right或者++left;如果是a[right] > a[keyI]或者a[left] < a[keyI]可能出现死循环,比如a[left] == a[right] == key时,交换完后不进入内部while,外部while陷入死循环。

主框架:
void _QuickSort(int* a, int begin, int end)
{if (begin >= end)return;//根据基准值把数组划分成左右序列int keyI = Partion(a, begin, end);//左右序列递归划分下去_QuickSort(a, begin, keyI - 1);_QuickSort(a, keyI + 1, end);
}void QucikSort(int* a, int n)
{_QuickSort(a, 0, n - 1);
}

上述为快速排序递归实现的主框架,与二叉树前序遍历规则非常像。

二叉树的递归终止条件是空树,快排的终止条件是数组只有一个元素(left==right)或者数组区间不存在(left>right)。

浅画一下展开图:

2.6.4 区间划分算法

前面所说,hoare划分法有一定的缺陷,我们下面重点介绍其他两种常用的划分方法。

hoare法
int Partion(int* a, int left, int right)
{int keyI = left;//left == right两个指针相遇,退出循环while (left < right){//right先找,right找小while (left < right && a[right] >= a[keyI]){right--;}//left找大while (left < right && a[left] <= a[keyI]){left++;}//都找到了,交换Swap(&a[left], &a[right]);}//left和right相遇,交换key和相遇位置元素Swap(&a[keyI], &a[left]);return left;
}
‍
挖坑法

步骤:

  1. 默认序列最左边的元素为基准值key,把值挖走用key变量保存,该位置为一个坑。

  2. 右边找小,找到后把值填给坑位,该位置成为新的坑位。

  3. 左边找大,找到后把值填给坑位,该位置成为新的坑位。

  4. 重复步骤2~3。

  5. 左右相遇,相遇位置也是个坑位,key值填入坑位。

图解:

代码:

int Partion2(int* a, int left, int right)
{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;
}

与前面代码不同的是,这里的key值我们不存下标,用一个变量保存。

前后指针法

步骤:

  1. 默认序列最左边的元素为基准值key,设置prev指针 == left,cur指针 == left+1。

  2. cur找小,找到后,prev++,a[prev]和a[cur]交换。

  3. 重复步骤2。

  4. cur走完以后,a[prev]和key交换。

图解:

代码:

int Partion3(int* a, int left, int right)
{int keyI = left;int prev = left, cur = prev + 1;while (cur <= right){if (a[cur] < a[keyI] && ++prev != cur)Swap(&a[prev], &a[cur]);++cur;}Swap(&a[prev], &a[keyI]);return prev; 
}

为了避免自己和自己交换,prev先++判断和cur是否相等,相等就不交换。

很明显这种分割方法的代码相比前面两种简单了许多,这种划分法也是最常用的。

2.6.5 快排优化

取key方面的优化

最理想的情况就是key值每次都是中间的值,快排的递归就是一个完美的二分。

快排在面对一些极端数据时效率会明显下降;就比如完全有序的序列,这种序列的基准值key如果再取最左边或者最右边的数,key值就是这个序列的最值,复杂度会变成O(N^2):

这时候就可以用三数取中法来解决这个弊端,三个数为:a[left],a[mid],a[right],这样就可以尽量避免key值选到最值的情况。  

//三数取中法选key值
int GetMidIndex(int* a, int left, int right) 
{int mid = (left + right) / 2; if (a[left] < a[mid]){if (a[mid] < a[right])return mid;else if (a[left] > a[right])return left; elsereturn right; }else  //mid < left{if (a[left] < a[right])return left;else if (a[mid] > a[right])return mid; elsereturn right; }
}
//前后指针划分
int Partion3(int* a, int left, int right)
{//中间值的下标为midI,a[left]替换为此中间值int midI = GetMidIndex(a, left, right);  Swap(&a[left], &a[midI]); int keyI = left;int prev = left, cur = prev + 1;while (cur <= right){if (a[cur] < a[keyI] && ++prev != cur)Swap(&a[prev], &a[cur]);++cur;}Swap(&a[prev], &a[keyI]);return prev; 
}
递归方面的优化

我们知道,递归深度太深并不是一件好事,所以我们可以针对递归方面来进行优化,减少绝大多数的递归调用。

如何优化呢?当递归到区间内元素个数<=10时,调用直接插入排序。

void _QuickSort(int* a, int begin, int end)
{if (begin >= end)return;//区间内元素个数 <= 10,调用直接插入排序if (end - begin + 1 <= 10){InsertSort(a + begin, end - begin + 1);//注意:起始地址是a + begin,不是a}else{//根据基准值把数组划分成左右序列int keyI = Partion3(a, begin, end); //左右序列递归划分下去_QuickSort(a, begin, keyI - 1); _QuickSort(a, keyI + 1, end); }
}

这种优化其实可以减少绝大多数的递归调用,我们把快排的递归划分想象成一颗二叉树,区间长度小于10的数组大概在这棵二叉树的最后三层,而最后三层占了整棵树结点个数的80%多(最后一层50%,倒数第二层25%...),类比快排的递归来看,我们省去了80%多的递归调用,并且对于数据规模较小的情况下,直插和快排的效率差不了多少,所以这是一个极大的优化,算法库中的sort函数也大多是这种优化。

2.6.6 快排非递归实现

快排的非递归我们可以使用一个栈(深度优先遍历)或者一个队列实现(广度优先遍历)

栈实现(代码+图解):
void QuickSortNonRByStack(int* a, int n)
{Stack st; StackInit(&st);int begin = 0, end = n - 1;//先Push右边界,在Push左边界//记住push顺序,取top的时候左右不要取反了StackPush(&st, end);StackPush(&st, begin);while (!StackEmpty(&st)){int begin = StackTop(&st);  StackPop(&st);  int end = StackTop(&st); StackPop(&st); int keyI = Partion3(a, begin, end);//[begin, keyI - 1]  keyI  [keyI + 1, end]//先递归到左区间,所以右区间先入栈if (keyI + 1 < end) {//先Push右边界,在Push左边界StackPush(&st, end);   StackPush(&st, keyI + 1);   }if (begin < keyI - 1){//先Push右边界,在Push左边界StackPush(&st, keyI - 1); StackPush(&st, begin);}}StackDestory(&st);
}

 

队列实现:
void QuickSortNonRByQueue(int* a, int n)
{Queue q;QueueInit(&q);int begin = 0, end = n - 1; QueuePush(&q, begin); QueuePush(&q, end); while (!QueueEmpty(&q)){int begin = QueueFront(&q);QueuePop(&q);int end = QueueFront(&q); QueuePop(&q); int keyI = Partion3(a, begin, end); if (begin < keyI - 1) {QueuePush(&q, begin);  QueuePush(&q, keyI - 1);}if (keyI + 1 < end) {QueuePush(&q, keyI + 1); QueuePush(&q, end);   }}QueueDestory(&q); 
}

2.6.7 特性总结

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

  • 时间复杂度:O(NlogN)

最好情况,每次key都在中间位置,正好二分

最坏情况,每次key都是最值,复杂度O(N^2)

平均情况(带优化),复杂度O(NlogN)

  • 空间复杂度:O(logN)

2.7 归并排序​

2.7.1 基本思想

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

2.7.2 排序步骤

归并排序和快速排序一样,采用的是分治法:

  1. 将待排序序列分成两个子序列。

  2. 递归两个子序列使两个子序列有序。

  3. 合并两个子序列使整个序列有序。

  4. 左右子序列的排序重复1~3.

2.7.3 代码实现

几个注意点:

  • 我们使用归并排序,能在原数组上进行归并吗?当然不能!假如后一个值小于前一个,那么就对前一个值进行了覆盖。所以我们要开辟一个临时空间,数据归并到这个临时空间里,然后再拷贝到原数组。这也是归并排序的一个缺点,需要额外空间。

  • tmp数组的数据拷贝到原数组时,注意两个空间的起始地址:tmp + left , a + left

void _MergeSort(int* a, int* tmp, int left, int right)
{if (left >= right)return;int  mid = (left + right) / 2;//[left, mid]  [mid + 1, right]_MergeSort(a, tmp, left, mid);_MergeSort(a, tmp, mid + 1, right);int begin1 = left, end1 = mid;int begin2 = mid + 1, end2 = right;int index = left;while (begin1 <= end1 && begin2 <= end2){if (a[begin1] <= a[begin2]){tmp[index++] = a[begin1++];  }else{ tmp[index++] = a[begin2++]; }}while (begin1 <= end1){ tmp[index++] = a[begin1++]; }while (begin2 <= end2){tmp[index++] = a[begin2++]; }//tmp局部排好的数据拷贝到a中//注意这里两个空间的起始地址,不要传tmp和a了memcpy(a + left, tmp + left, sizeof(int) * (right - left + 1));  
}void MergeSort(int* a, int n)
{int* tmp = (int*)malloc(sizeof(int) * n);if (tmp == NULL){perror("Merge:");exit(-1);}_MergeSort(a, tmp, 0, n - 1);free(tmp);
}

2.7.4 动图解析

2.7.5 非递归实现 

归并排序的非递归在逻辑上不难理解,但是他在边界上的控制却相当麻烦,我们先看一种不麻烦的情况:数组size为2^n的情况下。

假设数组个数为8,我们可以先一个数据和一个数据归并,然后两个数据和两个数据归并,然后四个数据和四个数据归并,完成排序。

void MergeSortNonR(int* a, int n) 
{int* tmp = (int*)malloc(sizeof(int) * n);if (tmp == NULL){perror("MergeSortNonR:");exit(-1);}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 index = begin1; //[begin1, end1]  [begin2, end2]两个区间进行归并while (begin1 <= end1 && begin2 <= end2){if (a[begin1] <= a[begin2]){tmp[index++] = a[begin1++];}else{tmp[index++] = a[begin2++];}}while (begin1 <= end1){tmp[index++] = a[begin1++];}while (begin2 <= end2){tmp[index++] = a[begin2++];}memcpy(a + i, tmp + i, sizeof(int) * (end2 - i + 1)); }gap *= 2;}free(tmp);
}
‍

但是我们并不能保证[begin1, end1][begin2, end2]这两个区间都存在,可能会有越界,我们可以肯定的是begin1一定不会越界,因为begin1 == i,而i < n,我们就是用i来控制一组数据归并的起始地址。所以可能越界的只会有:end1,begin2,end2

这三种越界我们根据右区间存不存在分两种谈论:

  1. end1和begin2越界,这种情况下第二个区间都不存在了,我们就不用归并了。

  2. end2越界,这种情况下第二个区间还存在,我们只需修正一下end2 = n - 1即可。

void MergeSortNonR(int* a, int n)
{int* tmp = (int*)malloc(sizeof(int) * n);if (tmp == NULL){perror("MergeSortNonR:");exit(-1);}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 index = begin1;//[begin1, end1]  [begin2, end2]两个区间进行归并//可能越界的位置:end1,begin2,end2//end1和begin2越界,右区间不存在,直接不用归了//end2越界,右区间还存在,修正一下end2 == n - 1,接着归if (begin2 >= n) //end1越界,begin2肯定越界{break;}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[index++] = a[begin1++]; }else{tmp[index++] = a[begin2++]; }}while (begin1 <= end1){tmp[index++] = a[begin1++]; }while (begin2 <= end2){tmp[index++] = a[begin2++]; }//一组归完直接拷贝,方便控制memcpy(a + i, tmp + i, sizeof(int) * (end2 - i + 1));  }gap *= 2;//printf("\n");}free(tmp); 
}

代码还有两点注意:

  • 一组数据归并时,begin1是一直++的,所以归并完后左区间的左边界并不是begin1,而是i。

  • 这里tmp数组拷贝到原数组的时候我们并不是拷贝整个数组,而是一组归并完后就直接拷贝,这样方便控制,不然我们不好控制拷贝的数据个数;如果整个tmp都拷贝,会带有随机值覆盖原数组,所以不建议整个拷贝。

2.7.6 特性总结

  1. 时间复杂度:O(N*logN) ,完美的二分

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

2.8计数排序

2.8.1 基本思想

计数排序相比于前面的所有排序都有所不同,因为这是一种非比较排序,也是一种哈希思维的应用。

哈希思维,就是健值和他的存储位置构成一种映射关系,也就是数组的值和他的下标存在一种映射关系。

2.8.2 排序步骤

排序步骤:

  1. 创建一个计数的数组,数组的值初始化为0。

  2. 遍历待排序数组,根据数组的值映射到计数数组的下标,让该下标的值++。

  3. 根据统计的结果,计数数组的数据覆盖到原数组中,完成排序。

这里有两点注意

  1. 但是我们不能进行直接映射(就是数组的值 == 映射的下标),这样很容易造成空间浪费。这里就要进行相对映射,先找到数组中的最小值min,映射下标 == 数组的值 - min。

  2. 计数数组的大小我们也不能随便开辟,大小应该为max - min + 1。

最终排序步骤:

  1. 找出数组的min和max。

  2. 创建一个计数的数组,大小为max - min + 1,数组的值初始化为0。

  3. 遍历待排序数组,根据数组的值映射到计数数组的下标,让该下标的值++。

  4. 根据统计的结果,计数数组的数据覆盖到原数组中,完成排序。

2.8.3 代码实现

void CountSort(int* a, int n)
{//找到max和minint max = a[0], min = a[0];for (int i = 1; i < n; i++){if (a[i] < min){min = a[i];}if (a[i] > max){max = a[i];}}//创建计数数组并初始化为0int range = max - min + 1;int* countArr = (int*)malloc(sizeof(int) * range);if (countArr == NULL){perror("CountSort:");exit(-1);}memset(countArr, 0, sizeof(int) * range); //统计数组数据for (int i = 0; i < n; i++){countArr[a[i] - min]++;}//根据统计结果,拷贝到原数组int index = 0; for (int i = 0; i < range; i++){while (countArr[i]--){a[index++] = i + min; }}free(countArr);
}
‍

2.8.4 特性分析

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

  2. 时间复杂度:O(N+range)

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

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

稳定性概念:

假定在待排序的记录序列中,存在多个具有相同的关键字的记录,若经过排序,这些记录的相对次序保持不变,即在原序列中,r[i]=r[j],且r[i]在r[j]之前,而在排序后的序列中,r[i]仍在r[j]之前,则称这种排序算法是稳定的;否则称为不稳定的。

下面我们研究一下七大排序的稳定性:

  • 直接插入排序:

待插入数据(tmp) >= a[end] 插入到该数据的后面,前后关系不变,直插排序是个稳定的排序

  • 希尔排序

假如两个相同的数据被分到了不同组,我们不能保证这两个数据的前后关系,所以希尔排序是个不稳定的排序

例如:

  • ​​​​​​​选择排序(最容易错的)

假如被交换的元素就是相同的元素,前后关系改变,所以选择排序是个不稳定的排序。

例如:

第一个3与最小元素1交换后,相同元素3的顺序发生了改变。

  • 堆排序

如果堆顶数据和他的一个孩子相同,前后关系改变,所以堆排序是个不稳定的排序

例如:

  • 冒泡排序

如果左边和右边相等,就不交换,这样前后关系不变,所以冒泡排序是个稳定的排序

  • 快速排序

假如一个值和key相等,并且相遇位置在这个值的右边,这样交换后前后关系改变,所以快速排序是个不稳定的排序

例如:

  • 归并排序

如果左区间的数 == 右区间的数时,把左区间的数归并下来,这样前后关系不变,所以归并排序是个稳定的排序

    //[begin1, end1]  [begin2, end2]两个区间进行归并while (begin1 <= end1 && begin2 <= end2){if (a[begin1] <= a[begin2]){tmp[index++] = a[begin1++];}else{tmp[index++] = a[begin2++];}}while (begin1 <= end1){tmp[index++] = a[begin1++];}while (begin2 <= end2){tmp[index++] = a[begin2++];}
‍
排序方法时间复杂度空间复杂度稳定性
直接插入排序O(N^2)O(1)Y
希尔排序O(N^1.3)O(1)N
选择排序O(N^2)O(1)N
堆排序O(NlogN)O(1)N
冒泡排序O(N^2)O(1)Y
快速排序O(NlogN)O(logN)N
归并排序O(NlogN)O(N)Y
计数排序O(N+range)O(range)N

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/143311.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

HarmonyOS开发:封装一个便捷的Log工具类

前言 日志打印&#xff0c;没什么好说的&#xff0c;系统已给我们提供&#xff0c;且调用也是非常的简单&#xff0c;我们封装的目的&#xff0c;一是扩展&#xff0c;打印一些不常见的类型&#xff0c;比如格式化json&#xff0c;使得日志看起来比较好看&#xff0c;二是&…

GEO生信数据挖掘(二)下载基因芯片平台文件及注释

检索到目标数据集后&#xff0c;开始数据挖掘&#xff0c;本文以阿尔兹海默症数据集GSE1297为例 目录 下载平台文件 1.AnnotGPL参数改为TRUE,联网下载芯片平台的soft文件。&#xff08;国内网速奇慢经常中断&#xff09; 2.手工去GEO官网下载 转换芯片探针ID为gene name 拓…

智能合约经典漏洞案例,xSurge 重入漏洞+套利 综合运用

智能合约经典漏洞案例&#xff0c;xSurge 重入漏洞套利 综合运用 1. 事件介绍 xSurge 被攻击事件发生在 2021-08-16 日&#xff0c;距离今天已经近 1 年了&#xff0c;为什么还会选择这个事件进行分析&#xff1f;主要是这个攻击过程很有意思&#xff0c;有以下的几点思考 使…

UG\NX二次开发 通过点云生成曲面 UF_MODL_create_surf_from_cloud

文章作者:里海 来源网站:《里海NX二次开发3000例专栏》 感谢粉丝订阅 感谢 Rlgun 订阅本专栏,非常感谢。 简介 有网友想做一个通过点云生成曲面的程序,我们也试一下 效果 代码 #include "me.hpp" /*HEAD CREATE_SURF_FROM_CLOUD CCC UFUN */

【2023年11月第四版教材】第15章《风险管理》(合集篇)

第15章《风险管理》&#xff08;合集篇&#xff09; 1 章节说明2 管理基础2.1 风险的属性2.2 风险的分类★★★2.3 风险成本★★★2.4 管理新实践 3 管理过程4 管理ITTO汇总★★★5 过程1-规划风险管理6 过程2-识别风险6.1 识别风险★★★6.2 数据收集★★★6.3 数据分析★★★…

基于微信小程序快递取件上门预约服务系统设计与实现(开题报告+任务书+源码+lw+ppt +部署文档+讲解)

文章目录 前言运行环境说明用户的主要功能有&#xff1a;管理员的主要功能有&#xff1a;具体实现截图详细视频演示代码参考论文参考源码获取 前言 &#x1f497;博主介绍&#xff1a;✌全网粉丝10W,CSDN特邀作者、博客专家、CSDN新星计划导师、全栈领域优质创作者&#xff0c;…

范数Norm-衡量向量大小的方法

性质 非负性: 范数的值总是非负的,且当且仅当向量全为零时,范数的值为零。 齐次性: 对于任意实数α,有 三角不等式: 对于任意向量x和y,有 常见范数 L1: 向量所有元素绝对值的和,权重稀疏 L2:欧几里得范数,权重平滑 无穷范数:表示向量中最大的元素 为什么使用范…

【VUE复习·6】监视属性watch:用途、两种写法、简写、应用时注意事项(重点)、深度监视(重点)

总览 1.监视属性是用来干什么的&#xff1f; 2.监视属性的两种写法 3.应用时注意事项 4.深度监视 一、监视属性是用来干什么的&#xff1f; 1.用途 监视一个值&#xff08;可以是基本属性 data&#xff0c;或者是计算属性 computed&#xff09;是否被改变。如果此值被改变&…

C语言-变量与数据类型

一、基本语法 1、注释 注释&#xff08;Comments&#xff09;可以出现在代码中的任何位置&#xff0c;用来向用户提示或解释代码的含义。程序编译时&#xff0c;会忽略注释&#xff0c;不做任何处理。 C 语言有两种注释方式&#xff1a; &#xff08;1&#xff09;单行注释 …

3+单基因泛癌+铜死亡纯生信思路

今天给同学们分享一篇3单基因泛癌铜死亡纯生信思路的生信文章“Systematic pan-cancer analysis identifies SLC31A1 as a biomarker in multiple tumor types”&#xff0c;这篇文章于2023年3月27日发表在BMC Med Genomics 期刊上&#xff0c;影响因子为3.622。 溶质载体家族3…

【Unity】简单的深度虚化shader

【Unity】简单的深度虚化shader 实现效果 可以用于对地图场景边界的白模处理 实现方法 1.关键方法 UnityObjectToClipPos&#xff1a;将物体坐标转换为屏幕坐标 LinearEyeDepth&#xff1a;将屏幕坐标中的z值转换为实际的深度值 saturate&#xff1a;将值规范到0~1之间&a…

【AI视野·今日Robot 机器人论文速览 第四十一期】Tue, 26 Sep 2023

AI视野今日CS.Robotics 机器人学论文速览 Tue, 26 Sep 2023 Totally 73 papers &#x1f449;上期速览✈更多精彩请移步主页 Daily Robotics Papers Extreme Parkour with Legged Robots Authors Xuxin Cheng, Kexin Shi, Ananye Agarwal, Deepak Pathak人类可以通过以高度动态…

成为吃鸡战场的王者!分享顶级战术干货,助您提高战斗力!

各位吃鸡战场的玩家们&#xff0c;欢迎来到本视频&#xff01;在这里&#xff0c;我将为您呈现一些与众不同的吃鸡干货&#xff0c;帮助您提高战斗力、轻松吃鸡&#xff01; 首先&#xff0c;让我们谈一谈作图工具推荐。绝地求生作图工具是吃鸡玩家们的必备利器。我将给大家推荐…

IDEA运行第一个Java简单程序(新建项目到运行类)

目录 前言 一、准备工作 JDK下载安装 1.IDEA下载安装 二、IDEA建立项目 &#xff08;一&#xff09;新建项目&#xff08;银河系&#xff09; &#xff08;二&#xff09;新建模块&#xff08;地球&#xff09; &#xff08;三&#xff09;新建包&#xff08;国家&#…

1.3python基础语法——PyCharm

1&#xff09;PyCharm的作用 python的集成开发环境&#xff0c;功能如下&#xff1a; Project管理 智能提示 语法高亮 代码跳转 调试代码 解释代码(解释器) 框架和库 2&#xff09;下载与安装 下载地址&#xff1a;http://www.jetbrains.com/pycharm/download/#sectionwind…

查询表中的全部列的数据

MySQL从小白到总裁完整教程目录:https://blog.csdn.net/weixin_67859959/article/details/129334507?spm1001.2014.3001.5502 语法格式: select from * 表名; 说明: * 表示所有列 由于不写where子句&#xff0c;表示无条件&#xff0c;找到所有的行&#xff01; 准备工作:执…

【计算机网络笔记九】I/O 多路复用

阻塞 IO 和 非阻塞 IO 阻塞 I/O 和 非阻塞 I/O 的主要区别&#xff1a; 阻塞 I/O 执行用户程序操作是同步的&#xff0c;调用线程会被阻塞挂起&#xff0c;会一直等待内核的 I/O 操作完成才返回用户进程&#xff0c;唤醒挂起线程非阻塞 I/O 执行用户程序操作是异步的&#xf…

909. 蛇梯棋

909. 蛇梯棋 题目-中等难度示例1. bfs 题目-中等难度 给你一个大小为 n x n 的整数矩阵 board &#xff0c;方格按从 1 到 n2 编号&#xff0c;编号遵循 转行交替方式 &#xff0c;从左下角开始 &#xff08;即&#xff0c;从 board[n - 1][0] 开始&#xff09;每一行交替方向…

基于Android+OpenCV+CNN+Keras的智能手语数字实时翻译——深度学习算法应用(含Python、ipynb工程源码)+数据集(四)

目录 前言总体设计系统整体结构图系统流程图 运行环境模块实现1. 数据预处理2. 数据增强3. 模型构建4. 模型训练及保存5. 模型评估6. 模型测试1&#xff09;权限注册2&#xff09;模型导入3&#xff09;总体模型构建4&#xff09;处理视频中的预览帧数据5&#xff09;处理图片数…

github搜索技巧

指定语言 language:java 比如我要找用java写的含有blog的内容 搜索项目名称包含关键词的内容 vue in:name 其他如项目描述跟项目文档&#xff0c;如下 组合使用 vue in:name,description,readme 根据Star 或者fork的数量来查找 总结 springboot vue stars:>1000 p…