数据结构之排序
Posted blknemo
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了数据结构之排序相关的知识,希望对你有一定的参考价值。
内部排序
分类 | 排序算法 | 改进思路 | 最好情况 | 平均时间复杂度 | 最坏情况 | 空间复杂度 | 稳定性 |
---|---|---|---|---|---|---|---|
插入排序 | 直接插入排序 | 基本排序方法 | O(n) | O(\(n^2\)) | O(\(n^2\)) | O(1) | 稳定 |
折半插入排序 | 确定有序序列的插入位置 | O(\(nlog_2n\)) | O(\(n^2\)) | O(\(n^2\)) | O(1) | 稳定 | |
希尔排序 | 利用直接插入的最好情况 | O(\(n^1.3\)) | NA | O(\(n^2\)) | O(1) | 不稳定 | |
交换排序 | 冒泡排序 | 基本排序方法 | O(n) | O(\(n^2\)) | O(\(n^2\)) | O(1) | 稳定 |
快速排序 | 交换不相邻元素 | O(\(nlog_2n\)) | O(\(nlog_2n\)) | O(\(n^2\)) | O(\(nlog_2n\)) | 不稳定 | |
选择排序 | 简单选择排序 | 基本排序方法 | O(\(n^2\)) | O(\(n^2\)) | O(\(n^2\)) | O(1) | 不稳定 |
堆排序 | 将待排序列转化为完全二叉树 | O(\(nlog_2n\)) | O(\(nlog_2n\)) | O(\(nlog_2n\)) | O(1) | 不稳定 | |
归并排序 | 2路归并排序 | 合并每个有序序列 | O(\(nlog_2n\)) | O(\(nlog_2n\)) | O(\(nlog_2n\)) | O(n) | 稳定 |
基数排序 | 基数排序 | O(d(n+r)) | O(d(n+r)) | O(d(n+r)) | O(r) | 稳定 |
插入排序
直接插入排序
有序序列L[1...i-1] | L(i) | 无序序列L[i+1...n] |
---|
- 基本思想:
可以将L(2)~L(n)依次插入到前面已排好序的子序列中,初始假定L[1]是一个已经排好序的子序列。- 查找出L(i)在L[1...i-1]中的插入位置k。
- 将L[k...i-1]中的所有元素全部后移一个位置。
- 将L(i)复制到L(k)。
- 具体实现:
void InsertSort(ElemType A[], int n)
int i, j;
for(i = 2; i <= n; i++) //依次将A[2]~A[n]插入到前面已排序序列
if(A[i].data < A[i-1].data) //若A[i]的关键码小于有序序列的最后一个元素(有序序列中的最大值)(即其前驱),需将A[i]插入有序表;大于有序序列的最后一个元素,则不用从头插入,并入有序序列(本来就排在有序序列之后),成为有序序列新的最大值(最后一个)
A[0] = A[i]; //复制为哨兵,A[0]不存放元素
for(j = i - 1; A[0].data < A[j].data; --j) //从后往前查找待插入位置
A[j+1] = A[j]; //向后挪位
A[j+1] = A[0]; //复制到插入位置
- 性能分析:
空间效率:O(1)
仅使用了常数个辅助单元。- 时间效率:O(\(n^2\))
在排序过程中,向有序子表中逐个插入元素的操作进行了n-1趟,每趟操作都分比较关键字和移动元素,而比较次数和移动元素取决于待排序表的初始状态。- 最好情况:O(n) (顺序)
- 最坏情况:O(\(n^2\)) (逆序)
- 平均情况:O(\(n^2\))
稳定性:稳定
由于每次插入元素时,总是从后向前先比较在移动,所以不会出现相同元素相对位置发生变化的情况。适用性:
适用于顺序存储和链式存储的线性表。
适用于基本有序的排序表和数据量不大的排序表。
为链式存储时,可以从前往后查找指定元素的位置。大部分排序算法都仅适用于顺序存储的线性表。
折半插入排序
基本思想:
将比较和移动操作分离,即先折半查找出元素的待插入位置,然后统一地移动待插入位置之后的所有元素。- 具体实现:
void InsertSort(ElemType A[], int n)
int i, j, low, high, mid;
for(i = 2; i <= n; i++) //依次将A[2]~A[n]插入前面的已知排序序列
A[0] = A[i]; //将A[i]暂存到A[0]
low = 1; //设置折半查找的范围
high = i-1;
while(low <= high) //折半查找(默认递增有序)
mid = (low + high) / 2; //取中间点
if(A[mid].data > A[0].data) //查找左半边
high = mid - 1;
else //查找右半边
low = mid + 1;
//查找结束后,high<low,k在high和high+1之间,所以顶替high+1位置,把high+1后面的元素后移
for(j=i-1; j>=high+1; --j)
A[j+1] = A[j]; //统一后移元素,空出插入位置
A[high+1] = A[0]; //插入
- 性能分析:
空间效率:O(1)
仅使用了常数个辅助单元。- 时间效率:O(\(n^2\))
仅减少了比较元素的次数,约为O(\(nlog_2n\)),该比较次数与待排序表的初始状态无关,仅取决于表中元素的个数n;而元素移动次数并未改变,他依赖于待排序表的初始状态。- 最好情况:O(\(nlog_2n\)) (顺序)
- 最坏情况:O(\(n^2\)) (逆序)
- 平均情况:O(\(n^2\))
稳定性:稳定
适用性:适用于顺序存储的线性表
希尔排序
基本思想:
先将待排序表分割成若干形如L[i, i+d, i+2d, ..., i+kd]的“特殊”子表,分别进行直接插入排序,当整个表中的元素已呈“基本有序”时,再对全体记录进行一次直接插入排序- 具体实现:
//对顺序表做希尔插入排序,本算法和直接插入排序相比,做了以下修改:
//1. 前后记录的位置增量是dk,不是1
//2. A[0]只是暂存单元,不是哨兵,当j<=0时,插入位置已到
void ShellSort(ElemType A[], int n)
for(dk=n/2; dk>=1; dk=dk/2) //步长变化
for(i=dk+1; i<=n; ++i)
if(A[i].data < A[i-dk].data) //需将A[i]插入有序增量子表
A[0] = A[i]; //暂存在A[0]
for(j=i-dk; j>0 && A[0].data<A[j].data; j-=dk)
A[j+dk] = A[j]; //记录后移,寻找插入的位置
A[j+dk] = A[0]; //插入
- 性能分析:
- 空间效率:O(1)
仅使用了常数个辅助单元 - 时间效率:
由于希尔排序的时间复杂度依赖于增量序列的函数,这设计数学上尚未解决的难题,所以其时间复杂度分析比较困难。- 最好情况:O(\(n^1.3\)) (当n在某个特定范围时)
- 最坏情况:O(\(n^2\))
- 平均情况:NA
稳定性:不稳定
但相同关键字的记录被划分到不同的子表时,可能会改变它们之间的相对次序。适用性:仅适用于线性表为顺序存储的情况。
- 空间效率:O(1)
交换排序
冒泡排序
基本思想:
假设待排序表长为n,从后往前(或从前往后)两两比较相邻元素的值,若为逆序(即A[i-1] > A[i]),则交换它们,知道序列比较完。称之为一趟冒泡,即将最小的元素交换到待排序列的第一个位置(关键字最小的元素如气泡一般逐渐往上“漂浮”直至“水面”)。下一趟冒泡时,前一趟确定的最小元素不在参与比较,待排序列减少一个元素。这样最多做n-1趟冒泡就能把所有元素排好序。- 具体实现:
//用冒泡排序法将序列A中的元素按从小到大排列
void BubbleSort(ElemType A[], int n)
for(i=0; i<n-1; i++) //趟数
flag = false; //表示本趟冒泡是否发生交换的标志
for(j=n-1; j>i; j--) //一趟冒泡过程
if(A[j-1].data > A[j].data) //若为逆序
swap(A[j-1], A[j]); //交换
flag = true;
if(flag == false) //本趟遍历后没有发生交换(前面一个元素均小于后面一个),说明表已经有序
return;
- 性能分析:
- 空间效率:O(1)
仅使用了常数个辅助单元 - 时间效率:O(\(n^2\))
当初始序列有序时,显然第一趟冒泡后没有元素交换(前均小于后),从而直接跳出循环,比较次数为n-1,移动次数为0。- 最好情况:O(n) (顺序)
- 最坏情况:O(\(n^2\)) (逆序)
- 平均情况:O(\(n^2\))
稳定性:稳定
i>j且A[i].data==A[j].data时,不会交换两个元素适用性:适用于线性表
- 空间效率:O(1)
快速排序
小L[1...k-1] | L(k) | 大L[k+1...n] |
---|
基本思想:
基于分治法,在待排序表L[1...n]中任取一个元素pivot(枢轴)作为基准,通过一趟排序将待排表划分为两个独立的两部分L[1...k-1]和L[k+1...n],使得L[1...k-1]中所有元素小于pivot,L[k+1...n]中的所有元素大于等于pivot,则pivot放在了其排序后的最终位置L(k)上,这个称为一趟快速排序。而后分别递归地对两个子表重复上述过程,直至每个部分内只有一个元素或空为止,即所有元素放在了其最终位置上。- 具体实现:
//划分算法(一趟排序过程)
int Partition(ElemType A[], int low, int high)
ElemType pivot = A[low]; //将当前表中第一个元素设为枢轴值,对表进行划分
while(low < high)
while(low<high && A[high]>=pivot) //当high中的值已经比pivot大,则不移动
--high;
//循环结束时,high中的元素比pivot小
A[low] = A[high]; //将小值移动到左端
while(low<high && A[low]<=pivot) //当low中的值已经比pivot小,则不移动
++low;
//循环结束时,low中的元素比pivot大
A[high] = A[low]; //将大值移动到右端
//循环结束时,low==high,已经是pivot的最终位置
A[low] = pivot; //放入最终位置
return low; //返回存放枢轴的最终位置
void QuickSort(ElemType A[], int low, int high)
if(low >= high) //不能划分了,递归出口
return;
int pivotpos = Partition(A, low, high); //划分
QuickSort(A, low, pivotpos-1); //依次对两个子表进行递归排序划分
QuickSort(A, pivotpos+1, high);
- 性能分析:
- 空间效率:O(\(log_2n\))
递归需要借助一个递归工作栈来保存每层递归调用的必要信息,其容量应与递归调用的最大深度一致。- 最好情况:?\(log_2(n+1)\)?
- 最坏情况:O(n)
- 平均情况:O(\(log_2n\))
- 时间效率:O(\(nlog_2n\))
快速排序的运行时间与划分是否对称有关,而后者又与具体使用的划分算法有关。- 最好情况:O(\(nlog_2n\)) (对称,即大于、小于枢轴的个数相等)
- 最坏情况:O(\(n^2\)) (基本有序或基本逆序)
- 平均情况:O(\(nlog_2n\))
稳定性:不稳定
在划分算法中,若右端区间有两个关键字相同,且均小于基准值的记录,则在交换到左区间后,它们的相对位置会发生变化。适用性:适用于顺序存储的线性表
- 空间效率:O(\(log_2n\))
选择排序
简单选择排序
基本思想:
假设排序表为L[1...n],第i趟排序即从L[i...n]中选择关键字最小的元素与L(i)交换,每一趟排序可以确定一个元素的最终位置,这样经过n-1趟排序就可使得整个排序表有序。- 具体实现:
//对表A做简单选择排序,A[]从0开始存放元素
void SelectSort(ElemType A[], int n)
for(i=0; i<n-1; i++) //一共进行n-1趟
min = i; //记录最小元素的位置
for(j=i+1; j<n; j++) //在A[i...n-1]中选择最小的元素
if(A[j] < A[min]) //更新最小元素位置
min = j;
if(min != i) //与第i个位置交换
swap(A[i], A[min]);
- 性能分析:
空间效率:O(1)
仅使用常数个辅助单元- 时间效率:O(\(n^2\))
元素间比较次数与序列的初始状态无关,始终是n(n-1)/2次。- 最好情况:O(\(n^2\))
- 最坏情况:O(\(n^2\))
- 平均情况:O(\(n^2\))
稳定性:不稳定
在第i趟找到最小元素后,和第i个元素交换,可能会导致第i个元素与其含有相同关键字元素的相对位置发生改变。适用性:适用于顺序存储的线性表
堆排序
基本思想:
具体实现:
- 性能分析:
空间效率:O(1)
仅使用了常数个辅助单元。- 时间效率:O(\(nlog_2n\))
建堆时间为O(n),之后又n-1次向下- 最好情况:O(\(nlog_2n\))
- 最坏情况:O(\(nlog_2n\))
- 平均情况:O(\(nlog_2n\))
稳定性:不稳定
进行筛选时,有可能把后面相同关键字的元素调整到前面。适用性:线性表
归并排序
基本思想:
“归并”的含义是将两个或两个以上的有序表组合成一个新的有序表。
假定待排序表含有n个记录,则可将其视为n个有序的子表,每个子表的长度为1,然后两两归并,得到\(?n/2?\)个长度为2或1的有序表;再两两归并……如此重复,知道合并成一个长度为n的有序表位置,这种排序方法称为2路归并排序递归形式的2路归并排序算法是基于分治的。
分解:将含有n个元素的待排序表分成各含\(n/2\)个元素的子表,采用2路归并排序算法对两个子表递归地进行排序。
合并:合并两个已排序的子表得到排序结果。具体实现:
- 性能分析:
空间效率:O(n)
Merge()操作中,辅助空间刚好占用n个单元- 时间效率:O(\(nlog_2n\))
每趟归并的时间复杂度为O(n),共需进行?log_2n?趟归并- 最好情况:O(\(nlog_2n\))
- 最坏情况:O(\(nlog_2n\))
- 平均情况:O(\(nlog_2n\))
稳定性:稳定
由于Merge()操作不会改变相同关键字记录的相对次序适用性:顺序存储的线性表
基数排序
基本思想:
将每个位数排好序,由低到高(由高到低)具体实现:
- 性能分析:
空间效率:O(r)
一趟排序需要的辅助空间为r(r个队列),但以后的排序中会重复使用这些队列- 时间效率:O(d(n+r))
基数排序需要进行d趟分配和收集,一趟分配需要O(n),一趟收集需要O(r)- 最好情况:O(d(n+r))
- 最坏情况:O(d(n+r))
- 平均情况:O(d(n+r))
稳定性:稳定
对于基数排序算法而言,按位排序是必须是稳定的,所以保证了基数排序的稳定性适用性:顺序存储的线性表
以上是关于数据结构之排序的主要内容,如果未能解决你的问题,请参考以下文章