数据结构 5排序算法
排序算法
- 插入排序
- 基本的插入排序(Insertion Sort)
- 希尔排序(shell Sort)
- 交换排序
- 基本的交换排序(bubble sort)
- 快速排序(quick sort)
- 归并排序(merge sort)
- 选择排序
- 基本的选择排序(selection sort)
- 堆排序 (heap sort)
- 基数排序(radix sort)
- 桶排序
- 排序算法的时间空间复杂度和稳定性
插入排序
基本插入排序(Insertion Sort)
如果要向一组已经排好了序的数中,再插入一个数,可以从头开始遍历,直到找到相应的位置,插入即可。
所以我们可以这样对一组数进行插入排序:进行N-1次 pass,在第P次pass,我们将位置P处的元素向前移动到相应的位置,前面P个元素一定是已经排好序的,后面的先不用管.(这就像将一个元素插入到一段已经排好序的数中)。例如P=1时,比较前P个元素(就是第0个),判断是否移动,做完后,保证了前2个元素是排好序的;然后P=2,将第2个元素移动到前两个元素的相应位置。
下面是插入排序的过程:
Original | 34 8 64 51 32 21 | 说明 |
---|---|---|
p=1 | 8 34 64 51 32 21 | 8与34比较,8向前移动 |
p=2 | 8 34 64 51 32 21 | 64 与前面的8 34 比较,不用变 |
p=3 | 8 34 51 64 32 21 | 51 与前面的8 34 64 比较,51移动到34后面 |
p=4 | 8 32 34 51 64 21 | 32移动到34的前面 |
p=5 | 8 21 32 34 51 64 | 21移动到32的前面 |
插入排序的例程:
void InsertionSort(int A[], int N)
{int j, p; //p表示当前正移动的元素下标int Temp;for (p = 1; p < N;p++){Temp = A[p];//下面的方法可以代替swap方法,更加快for (j = p; j > 0 && A[j - 1] > Temp;j--){A[j] = A[j - 1]; //将前面一个数向后移一位,前面数的位置空出来}//不断将比位置P大的数后移A[j] = Temp;//最后将Temp放入最终空出来的那个位置}
}
循环不变式:第p次pass,定义j=p
,每次比较A[j-1]
与A[p]
,如果A[j] > A[p]
,将A[j]
后移,即A[j] = A[j-1]
。
循环结束的条件: j > 0 && A[j-1] > A[p]
从条件A[j-1] < Temp可以看出, 原序列越有序,时间就越小
插入排序的平均时间复杂度为O(N2).当数组已经排好序时,时间复杂度最优为O(N)O(N)O(N)
定理: Any algorithm that sorts by exchanging adjacent elements requires Ω(N2)Ω(N^2)Ω(N2) time on average
希尔排序
先知道Hk-sorted是什么: 排序后,对于所有的i,A[i] <= A[i+ Hk] 都成立。就说,如果Hk=5的话,A[0]<=A[5]<=A[10]<=… ; A[1]<=A[6]<=A[11]<=…;相差5的元素一定是排好序了的。
看一个排序的例子:
Original : 81 94 11 96 12 35 17 95 28 58 41 75 15
After 5-sort: 35 17 11 28 12 41 75 15 96 58 81 94 95
After 3-sort: 28 12 11 35 15 41 58 17 94 75 81 96 95
After 1-sort: 11 12 15 17 28 35 41 58 75 81 94 95 96
定义一个increment sequence(步长序列) H1<H2<H3<…<Hk.
Shell排序就是按顺序进行Hk-sort, Hk-1-sort, …H1-sort,只要保证H1=1,任何序列都能进行shell排序。
如果只进行1-sort,那么这等价于插入排序;
之所以在1-sort之前,使用了5-sort,3-sort,这会使一列数变得大致有序,前面说了原序列越有序,插入排序的时间越少。所以等到1-sort的时候,并不需要比较太多次。
Hk-sorted相当于步长为Hk的插入排序
shell排序的时间复杂度,取决于H1<H2<H3<…<Hk.这个序列的选取,下面的代码使用Shell’s increment sequence:,对于N个数据,步长序列取:N/2,N/4,N/8,N/16,…1;
void ShellSort(int A[], int N)
{int i, j, Increment;int Tmp;//h sequencefor (Increment = N / 2; Increment > 0;Increment/=2){//Increment=1时,这块代码与插入排序完全相同for (i = Increment; i < N;i++){Tmp = A[i];for (j = i; j >= Increment;j-=Increment){if(Tmp<A[j-Increment])A[j] = A[j - Increment];elsebreak;}A[j] = Tmp;}}
}
使用希尔增量时,最坏情况下的运行时间为O(N2)O(N^2)O(N2),如序列1 9 2 10 3 11 4 12 5 13 6 14 7 15 8 16。使用8-sort 4-sort 2-sort 均不会产生任何效果。最后只有1-sort相当于插入排序,起作用。
使用一些其他的增量,如Hibbard增量可以改善时间复杂度,因此希尔排序的时间复杂度取决于增量序列的选择。
堆排序(Heapsort)
算法1:假设我们对数组H[]中的元素进行从小到大排序
- 将数组H[]调成最小堆
- 新建一个TmpH[]数组
- 每次对堆H进行一次删除,由堆的性质可知,这个元素一定的最小的那一个,将其放入TmpH[]中;
- 不断重复3,知道所有的元素都删除了
- 将TmpH中的元素返回给H数组
建堆的时间复杂度为O(N)O(N)O(N)
删除最小元素的时间复杂度为O(logN)O(logN)O(logN)
总共进行N次删除,所以总的时间复杂度为O(NlogN)O(NlogN)O(NlogN)
这种算法会消耗额外一倍的内存空间,所以我们常采用另一种算法。
算法2
- 先将大小为N的数组A调成最大堆(元素A[0]一定最大)
- 将A[0]与A[N-1]交换,并将堆的大小减1,单独对第一个不符合的元素调整,将大小为N-1的数组A调成堆(元素A[0]又变成最大了,且先前的在A[N],不在堆中,不受影响)
- 不断重复步骤2,直到最后堆的大小减小为0
- 此时数组A就是按照从小到大排序的
注意: 从小到大排序,使用最大堆;从大到小排序,使用最小堆。
由于给入的数组从0开始记录数据,所以调成堆的时候,也从零开始计数,要格外注意,位置 i 处的,左儿子为 2i+1, 右儿子为 2i+2
void HeapSort(int a[], int N)
{int i;for (i = N/2; i >=0; i--) PercDown(a, i, N); //先将数组a调成最大堆for (i = N - 1; i > 0;i--){Swap(&a[0],&a[i]); //将最大的元素放到后面,堆大小-1PercDown(a, 0, i); //重新调成堆,注意这里数组变小了}
}
void PercDown(int a[],int index, int N)
{int child; int tmp = a[index];for (i = index; i * 2 + 1 < N; i = child){child = i * 2 + 1; if (child != N-1 && a[child + 1] > a[child]) child++; if (tmp < a[child]) a[i] = a[child]; elsebreak; }a[i] = tmp;
}
归并排序
归并排序就是一个典型的分治法的例子,将一个数列分成两部分,分别排序,然后合并(不断地从两个数组中的第一个元素选择较小者,直到整个数组被填满)。
算法:首先判断数组大小,若无元素或只有一个元素,则数组必然已被排序;若包含的元素多于1个,则执行下列步骤:
- 把数组分为大小相等的两个子数组(第一个数组大小为n/2,第二个为n-n/2);
- 对每个子数组递归采用归并算法进行排序;
- 合并两个排序后的子数组。
void Merge (int array[], int arr1[], int n1, int arr2[], int n2)
{ int p, p1, p2; p = p1 = p2 = 0; while (p1 < n1 && p2 < n2) { if (arr1[p1] < arr2[p2]) array[p++] = arr1[p1++]; else array[p++] = arr2[p2++]; } while (p1 < n1) array[p++] = arr1[p1++]; while (p2 < n2) array[p++] = arr2[p2++];
}
void SortIntegerArray (int array[], int n)
{ int i, n1, n2, *arr1, *arr2; if (n > 1) { n1 = n / 2; n2 = n – n1; arr1 = NewArray (n1, int); arr2 = NewArray (n2, int); for (i = 0; i < n1; i++) arr1[i] = array[i]; for (i = 0; i < n2; i++) arr2[i] = array[n1 + i]; SortIntegerArray (arr1, n1); SortIntegerArray (arr2, n2); Merge (array, arr1, n1, arr2, n2); FreeBlock (arr1); FreeBlock (arr2); }
}
如果对Merge的每个递归调用均局部声明一个临时数组,那么在任意时刻,就可能有logN个数组。我们需要每次递归让它释放掉临时空间,任意时刻只需要有一个临时数组活动即可,空间复杂度为O(N)
Note:Mergesort requires linear extra memory, and copying an array is slow. It is hardly ever used for internal sorting, but is quite useful for external sorting.
时间复杂度分析:
T(1)=1T(1) = 1T(1)=1
T(N)=2T(N/2)+O(N)=2kT(N/2k)+k∗O(N)T(N)=2T(N/2)+O(N)=2^kT(N/2^k)+k*O(N)T(N)=2T(N/2)+O(N)=2kT(N/2k)+k∗O(N)
2k=N2^k = N2k=N
T(N)=NT(1)+logN∗O(N)=O(N+NlogN)T(N)=NT(1)+logN*O(N)=O(N+NlogN)T(N)=NT(1)+logN∗O(N)=O(N+NlogN)
快速排序
快速排序是已知的最快的排序算法,平均时间复杂度为O(N logN),最坏情况为O(N2)。
基本的算法思路:
分治递归的算法,当数组中只有一个或两个元素的时候为递归出口,每次递归,选取一个标准元素pivot,将所有小于pivot的元素移到pivot左边,所有大于pivot的元素移到pivot右边,然后对左右两边的元素做相同的操作,直到只有一或两个元素。
void QuickSort(int A[], int N)
{if(N<2) return;pivot = pick any element v int A[];//将A划分成两个不相交集合,一个集合所有元素都小于pivot,另一个所有元素都大于pivotA1 = {x∈A-{pivot}|x <= pivot};A2 = {x∈A-{pivot}|x >= pivot};return {QuickSort(A1) ∪ pivot ∪ QuickSort(A2)};
}
如何选取pivot
我们经常用数组的第一个元素作为pivot,这样可以是可以,但必须保证输入是随机的。如果一个数组的元素已经排好了序,再进行快速排序,会消耗O(N2)的时间,而且还是做无用功。所以尽量不用这种选择办法。
我们会想到使用随机算法随机选取一个pivot,但是随机算法本身也会消耗一定的时间。
Median-of-Three Partitioning
我们采用这种方法:选择一组数据中最左边的,最右边的,和中间的,这三个元素,将三个元素中间大的元素作为pivot。
划分方法
如何只在原数组上进行操作,达到这样的效果:
所有比pivot小的元素在pivot的左边,所有比pivot大的元素在pivot的右边。
首先,将pivot与最后一个元素交换。令i指向第一个元素,j指向倒数第二个元素
然后,当 i 在 j 的左边的时候,
- 如果 A[i] 小于pivot ,就将 i 向右移动,直到A[i] >pivot
- 如果 A[j] 大于pivot, 就将 j 向左移动,直到A[j] <pivot
当 i,j 都停下来时,将A[i] 与 A[j] 的值交换。这样做的效果是,使比pivot大的数都在右边,比pivot小的数都在左边。
重复上面的步骤:
当 i 与 j 相交(i 跑到 j 的右边了)时,这个时候便不再将i,j对应的元素交换。
执行最后一个步骤,将 i 对应的元素与 pivot 交换(因为此时i指向的元素比pivot大,换了以后,正好把指向的元素放到了后面)
这样就满足了划分的要求。
考虑一下当 i 或 j 指向的元素与pivot相等的时候,是否要停下。
答案是都要停下。想象一种情况,如果对一组元素都相同的数进行快排。
不停下的话,i就会一直向右边移动,最后 i 的指向与pivot相同,这样划分出来的两个集合太不平衡了(pivot在最后面),这就像前面说过的用第一个元素作为pivot,对一个已经排序好的元素进行排序一样,会消耗O(N2)时间。
而如果停下的话,虽然每次会进行交换,但最终pivot会移到中间位置,这样对两边继续进行递归会减少更多的时间,时间复杂度为O(N logN)。
元素较少的情况
当元素个数小于等于20左右时,插入排序反而比快速排序更快。解决方法是判断N的大小,当N比较小时,采用其他的排序方法。
实现代码
下面是选择pivot的代码。这里将Left,Center,Right位置的数先排好了顺序,即满足 A[Left]<=A[Center]<=A[Right],这样有很多好处。
将pivot与Right-1位置处的数交换,并将 i 设置为Left+1,j 设置为Right-2。A[Left]与A[Right]的数起到了边界的效果。因为数组两边的数一定一个小于pivot,一个大于pivot,所以i,j移动的时候,就不会越界。
int Median3(int A[], int Left, int Right)
{int Center = (Left + Right) / 2;//将A[Left]<=A[Center]<=A[Right],先排好了顺序if(A[Left] > A[Center])Swap(&A[Left], &A[Center]);if(A[Left] > A[Right])Swap(&A[Left], &A[Right]);if(A[Center] > A[Right])Swap(&A[Center], &A[Right]);Swap(&A[Center], &A[Right - 1]); //将pivot放到了Right-1的位置return A[Right - 1];
}
#define Cutoff (2)void qsort(int A[],int Left,int Right)
{int i, j, pivot;if(Left + Cutoff<= Right) //当序列过短时,调用其他的排序方法{pivot = Median3(A, Left, Right);i = Left;j = Right - 1;for (;;){while(A[++i]<pivot){} while(A[--j]>pivot){}if(i<j)Swap(&A[i], &A[j]);elsebreak;}Swap(&A[i], &A[Right - 1]);qsort(A, Left, i - 1);qsort(A, i + 1, Right); }else{InsertionSort(A + Left, Right - Left + 1);}
}void Quicksort(int A[], int N)
{qsort(A, 0, N - 1);
}
注意,当序列过短时,应该调用其他的排序方法。否则Median3会出问题。
为什么不可以这么写?
i = left +1; j = Right - 2;
for(; ; )
{
while(A[i]<pivot) i++;
while(A[j]>pivot) j++;
…
}
注意不能先判断A[i],是否小于pivot,再将i++;这样写的话,当A[i]=A[j]=pivot时,会无限循环,i,j再也移动不了了。所以要先移动,再判断。
一些其他问题
采用递归方式对顺序表进行快速排序,下列关于递归次数的叙述中,正确的是(D)
A递归次数与初始数据的排列次序无关
B每次划分后,先处理较长的分区可以减少递归次数
C每次划分后,先处理较短的分区可以减少递归次数
D递归次数与每次划分后得到的分区处理顺序无关
https://www.nowcoder.com/questionTerminal/69fc9122a0a74b5f8e011c4f53419dd3?pos=7&mutiTagIds=591&orderByHotValue=1
递归次数,取决于递归树,而递归树取决于轴枢的选择。树越平衡,递归次数越少。而对分区的长短处理顺序,影响的是递归时对栈的使用内存,而不是递归次数
对大的结构进行排序
我们常常想对一个结构进行排序。比如学生这个结构可能包括学号,姓名等信息。我们想根据学生的成绩进行排序。如果直接交换两个结构,额外的开销会非常大。因此我们可以添加一个指针指向结构,对指针进行排序即可。
桶排序(Bucket Sort)
Any algorithm that sorts by comparisons only must have a worst case computing time of Ω( N log N ).
通过比较进行的算法至少要NlogN时间,但在一些特殊的情况下,排序的时间复杂度可以达到线性时间。例如桶排序。
有N个学生,他们的成绩在0-100之间,现在需要对这N个学生的成绩进行排序。
从题目中可以看出,N个数字进行排序,不同的数字个数最多只有M=101个,我们可以使用一个数组指针A[101],对N个学生的成绩进行遍历,若读到学生m1的成绩为X, 便使A[X]指向学生m1;如果又读到一个学生m2的成绩也为X,将m2这个结构作为节点插入链表A[X]中。
这样排序所需的时间为O(M,N)
基数排序(Radix Sort)
给出N个范围在0-999(M=1000)的正整数,如何在线性时间内将这N个数排好序?
算法思想:
假设有编号0-9的十个桶,待排序的数170, 45, 75, 90, 02, 802, 2, 66
先看最低位,依照最低位将数放到相应的桶中。获得如下的序列;
170, 90, 02, 802, 2, 45, 75, 66
为什么170在90的前面,他们的个位都是0啊?
因为原序列中170在90的前面,所以按低位排了一遍后,170还是在90前。
再看次低位,比较后获得序列:
02, 802, 02, 45, 66, 170, 75, 90
为什么802在02的前面?因为次低位排序之前,就在前面了。不改变原来的相对顺序
最后对最高位进行一次,就排序成功了
002, 002, 045, 066, 075, 090, 170, 802
冒泡排序与选择排序
冒泡排序是相邻两个元素交换,小的元素向上冒。
选择排序是所有元素中选择最小的放第一个,剩下的元素中最小的放第二个。
排序算法的时间空间复杂度及稳定性
排序算法 | 时间复杂性 | 空间复杂性 | 稳定性 |
---|---|---|---|
Insertion sort | 最优O(N)O(N)O(N)最坏O(N2)O(N^2)O(N2)平均O(N2)O(N^2)O(N2) | O(1)O(1)O(1) | 稳定 |
Shell sort | 不同的增量的选取,时间复杂度不同 | O(1)O(1)O(1) | 不稳定 |
Selection sort | 最优最坏平均O(N2)O(N^2)O(N2) | O(1)O(1)O(1) | 不稳定 |
Bubble sort | 最优O(N)O(N)O(N)最坏O(N2)O(N^2)O(N2)平均O(N2)O(N^2)O(N2) | O(1)O(1)O(1) | 稳定 |
Heap sort | 最优最坏平均O(NlogN)O(NlogN)O(NlogN) | O(1)O(1)O(1) | 不稳定 |
Merge sort | 最优最坏平均O(NlogN)O(NlogN)O(NlogN) | O(N)O(N)O(N) | 稳定 |
Quick sort | 最优O(NlogN)O(NlogN)O(NlogN)最坏O(N2)O(N^2)O(N2)平均O(NlogN)O(NlogN)O(NlogN) | O(log2N)O(log_2N)O(log2N) | 不稳定 |
- 归并排序与快速排序相比,归并排序空间消耗更大,但它是稳定的排序,而且快速排序最坏情况的时间复杂度为O(N2)O(N^2)O(N2)
- 冒泡排序与选择排序相比,冒泡排序是稳定的
- 基本的插入排序与希尔排序相比,插入排序是稳定的
数据结构 5排序算法相关推荐
- 常用数据结构以及数据结构的排序算法
2019独角兽企业重金招聘Python工程师标准>>> 数组 (Array) 在程序设计中,为了处理方便, 把具有相同类型的若干 变量按有序的形式组织起来.这些按序排列的同类数据元素 ...
- 【数据结构】——排序算法——2.1、冒泡排序
[数据结构]--排序算法--2.1.冒泡排序 一.先上维基的图: 图一.冒泡排序 分类 排序 ...
- 【数据结构】排序算法及优化整理
排序算法 排序算法 选择排序 Selection Sort 插入排序 Insertion Sort 归并算法 Merge Sort 快速排序 Quick Sort 堆排序 Heap Sort 二叉堆的 ...
- 数据结构_排序算法总结
作者丨fredal https://www.jianshu.com/p/28d0f65aa6a1 所有内部排序算法的一个总结表格 简单选择排序 首先在未排序序列中找到最小(大)元素,存放到排序序列的起 ...
- 【恋上数据结构】排序算法前置知识及代码环境准备
排序准备工作 何为排序? 何为稳定性? 何为原地算法? 时间复杂度的知识 写排序算法前的准备 项目结构 Sort.java Asserts.java Integers.java Times.java ...
- 【数据结构排序算法系列】数据结构八大排序算法
排序算法在计算机应用中随处可见,如Windows操作系统的文件管理中会自动对用户创建的文件按照一定的规则排序(这个规则用户可以自定义,默认按照文件名排序)因此熟练掌握各种排序算法是非常重要的,本博客将 ...
- 【数据结构】排序算法
1,概念 在最好情况下,直接插入排序.冒泡排序的 时间复杂度最低. 在评价情况下,直接快速排序.堆排序.归并排序的 时间复杂度最低. 1)插入排序和选择排序 插入排序:直接插入排序.折半插入排序.2- ...
- 【数据结构】排序算法总结及代码实现
我们通常说的排序算法指的是内部排序算法,即数据在内存中进行排序. 首先先来看一下我们学过的排序都有什么? 排序可以大的方面分为比较排序和非比较排序? 比较排序有: 1.冒泡排序 2.选择排序 3.插入 ...
- 数据结构:排序算法总结
常用排序算法时空复杂度及稳定性: 排序算法 时间复杂度平均情况 时间复杂度最好情况 时间复杂度最坏情况 辅助空间 稳定性 冒泡排序 O(n^2) O(n) O(n^2) O(1) 稳定 选择排序 O( ...
- 【数据结构】排序算法总结
在待排序的文件中,若存在多个关键字相同的记录,经过排序后这些具有相同关键字的记录之间的相对次序保持不变,该排序方法是稳定的:若具有相同关键字的记录之间的相对次序发生改变,则称这种排序方法是不稳定的.即 ...
最新文章
- Windows使用CLion 远程调试Linux程序
- 理解smart pointer之三:unique_ptr
- php 时间戳 时区,PHP时间函数 时间戳 设置时区
- Java爬取校内论坛新帖
- 欧几里得算法和扩展欧几里得算法(Euclidean_Algorithm and Extended_Euclidean_Algorithm)
- mysql 逐列读取_mysql – 根据其他列如何使用逐列
- python中的range与list函数
- es6 Promise.prototype.catch()方法
- HALCON 21.11:深度学习笔记---对象检测, 实例分割(11)
- java 密码生成器_[Java小白]WIFI纯数字密码字典生成器
- LaTeX论文排版参考文献格式转换
- win10计算机错误代码,win10电脑更新失败提示错误代码0x80070424修复方法
- 校招总结--建议全文背诵
- 回顾|伍鸣博士出席《华人之光-世界瞩目的华人 Web3 项目》圆桌论坛
- QQ浏览器x5内核的兼容性问题
- Python爬取两个城市之间的直线距离
- 关于图文识别功能相关技术的实现
- python seo cms_巧用帝国CMS系统变量提升网站用户体验 完善SEO优化
- 米家小相机最新固件_能拍4K的米家小相机只要699了,你还要啥自行车?!
- 解决【v-show 有时失效】问题