数据结构 | 考研八大内部排序算法
Posted 即刻笔记CS
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了数据结构 | 考研八大内部排序算法相关的知识,希望对你有一定的参考价值。
一、内排序和外排序的区别
排序(Sort):就是重新排列表中的元素,使表中的元素满⾜按关键字有序的过程。
内部排序:数据都在内存中。(关注如何使算法的时间、空间复杂度更低)
外部排序:待排序记录的数量很大,以致于内存不能一次容纳全部记录,所以在排序过程中需要对外存进行访问的排序过程。(关注如何减少 IO 次数)
二、内部排序
插入排序、希尔排序、冒泡排序、快速排序、选择排序、堆排序、归并排序、基数排序。
头文件及测试数据:
#include<stdio.h>
#include<algorithm>
using namespace std;
//打印测试函数
void print(int a[], int n ,int i){
printf("%d:",i);
for(int j=0; j<8; j++){
printf("%d",a[j]);
}
printf("\n");
}
//测试数据
int main(int argc, char const *argv[])
{
int a[10] = {3, 2, 7, 6, 5, 8, 4, 1, 9, 0};
Sort(a, 10);
return 0;
}
1、插入排序
-
直接插入排序
算法思想:每次将⼀个待排序的记录按其关键字⼤⼩插⼊到前⾯已排好序的⼦序列中, 直到全部记录插⼊完成。
示意图:
代码实现:
//插入排序
void InsertSort(int *a,int n){
int temp;
for (int i = 1; i <= n; i++)
{
temp = a[i];
int j=i;
while (temp < a[j - 1])
{
a[j ] = a[j-1];
j--;
}
a[j] = temp;
print(a, 10, i);
}
}
输入:{3, 2, 7, 6, 5, 8, 4, 1, 9, 0}
输出:
1:2376584190
2:2376584190
3:2367584190
4:2356784190
5:2356784190
6:2345678190
7:1234567890
8:1234567890
9:0123456789
算法效率分析:
空间复杂度:O(1)
最好时间复杂度(全部有序):O(n)
最坏时间复杂度(全部逆序):O(n^2)
平均时间复杂度:O(n^2)
算法稳定性:稳定
-
折半插入排序
算法思路:先⽤折半查找找到应该插⼊的位置,再移动元素。
代码实现:
//折半插入排序
void BInsertSort(int a[],int size){
int i,j,low = 0,high = 0,mid;
int temp = 0;
for (i=1; i<size; i++) {
low=0;
high=i-1;
temp=a[i];
//采用折半查找法判断插入位置,最终变量 low 表示插入位置
while (low<=high) {
mid=(low+high)/2;
if (a[mid]>temp) {
high=mid-1;
}else{
low=mid+1;
}
}
//有序表中插入位置后的元素统一后移
for (j=i; j>low; j--) {
a[j]=a[j-1];
}
a[low]=temp;//插入元素
}
print(a, 10, i);
}
输入:{3, 2, 7, 6, 5, 8, 4, 1, 9, 0}
输出:
1:2376584190
2:2376584190
3:2367584190
4:2356784190
5:2356784190
6:2345678190
7:1234567890
8:1234567890
9:0123456789
算法效率分析:
⽐起“直接插⼊排序”,⽐较关键字的次数减少了,但是移动元素的次数没变, 整体来看时间复杂度依然是O(n^2)。
2、希尔排序
希尔排序,又称“缩小增量排序”,也是插入排序的一种,但是同前面几种排序算法比较来看,希尔排序在时间效率上有很大的改进。
算法思想:先将整个记录表分割成若干部分,分别进行直接插入排序,然后再对整个记录表进行一次直接插入排序。
示意图:
代码实现:
//希尔排序
void SheelSort(int *a,int n){
int gap = n / 2;
for ( ; gap>0; gap = gap / 2)
{
for (int i = gap; i <= n; i++)
{
int temp = a[i];
int j;
for (j = i-gap; j>=0&&temp<a[j]; j-=gap)
{
a[j + gap] = a[j];
}
a[j + gap] = temp;
}
print(a, 10, gap);
}
}
输入:{3, 2, 7, 6, 5, 8, 4, 1, 9, 0}
输出:
5:3216084795
2:0215364798
1:0123456789
算法效率分析:
时间复杂度:O(nlogn)
时间复杂度:和增量序列 d1, d2, d3… 的选择有关,⽬前⽆法⽤数学⼿段证明确切的时间复杂度最坏时间复杂度为 O(n^2 ),当n在某个范围内时,可达O(n^1.3)
非稳定排序
原地排序
3、冒泡排序
算法思想: 将无序表中的所有记录,通过两两比较关键字,得出升序序列或者降序序列。
示意图:
代码实现:
//冒泡排序
void BubbleSort(int *a,int n){
int count = 0;
for (int i = 0; i < n-1; i++)
{
count = 0;
for (int j = n-1; j > i; j--)
{
if(a[j-1]>a[j])
{
count = 1;
swap(a[j-1], a[j]);
}
}
if (count==0)
break;
print(a, 10, i);
}
}
输入:{3, 2, 7, 6, 5, 8, 4, 1, 9, 0}
输出
0:0327658419
1:0132765849
2:0123476589
3:0123457689
4:0123456789
算法效率分析:
空间复杂度:O(1)
最好时间复杂度(全部有序):O(n)
最坏时间复杂度(全部逆序):O(n^2)
平均时间复杂度:O(n^2)
算法稳定性:稳定
⭐4、快速排序
注:C语言中自带函数库中就有快速排序——qsort函数 ,包含在 <stdlib.h> 头文件中。
算法思想:通过一次排序将整个无序表分成相互独立的两部分,其中一部分中的数据都比另一部分中包含的数据的值小,然后继续沿用此方法分别对两部分进行同样的操作,直到每一个小部分不可再分,所得到的整个序列就成为了有序序列。
-
先将 A[0]存至某个临时变量 temp,并令两个下标 left、right 分别指向序列首尾(如令left = 0、right = n)。 -
只要 right 指向的元素 A[right]大于 temp,就将 right 不断左移;当某个时候 A[right]≤temp 时,将元素 A[right]挪到 left 指向的元素 A[left)处。 -
只要left 指向的元素 A[left]不超过 temp,就将 left 不断右移;当某个时候 A[left]>temp时,将元素 A[left]挪到 right 指向的元素 A[right]处。 -
重复2、3,直到left 与 right相遇,把 temp(也即原 A[1])放到相遇的地方。
示意图:
代码实现:
//对区间[left,right]进行划分
int Partition(int *a,int left,int right){
int temp = a[left];
while (left<right)
{
while (left<right && a[right]>temp)
right--;
a[left] = a[right];
while (left<right && a[left]<=temp)
left++;
a[right] = a[left];
}
a[left] = temp;
return left;
}
//快速排序
void QuickSort(int *a,int left,int right){
if(left<right){
int pos = Partition(a, left, right);
QuickSort(a, left, pos - 1);
QuickSort(a, pos + 1, right);
}
}
输入:{3, 2, 7, 6, 5, 8, 4, 1, 9, 0}
输出:
0, 1, 2, 3, 4, 5, 6, 7, 8, 9
算法效率分析:
空间复杂度:最好:O(n)。最坏:O(log n)。
最好时间复杂度(每次划分很平均):O(n log n)
最坏时间复杂度(原本正序或逆序):O(n^2)
平均时间复杂度:O(n log n)
算法稳定性:不稳定
5、简单选择排序
算法思想:对于具有 n 个记录的无序表遍历 n-1 次,第 i 次从无序表中第 i 个记录开始,找出后序关键字中最小的记录,然后放置在第 i 的位置上。
示意图:
代码实现:
//简单选择排序
void SelectSort(int a[],int n){
int temp;
for(int i=0;i<n-1;i++){
temp=i;
for(int j=i+1;j<n;j++){
if(a[temp]>a[j]){
temp=j;
}
}
swap(a[i],a[temp]);
print(a, 10, i);
}
}
输入:{3, 2, 7, 6, 5, 8, 4, 1, 9, 0}
输出:
0:0276584193
1:0176584293
2:0126584793
3:0123584796
4:0123485796
5:0123458796
6:0123456798
7:0123456798
8:0123456789
算法效率分析:
空间复杂度:O(1)
时间复杂度:O(n^2)
算法稳定性:不稳定
6、堆排序
6.1 堆排序
堆的定义: 堆是一颗二叉树,树中每个节点的值都不小于(或者不大于)其左右孩子节点的值。其中,父节点的值 大于或等于 孩子结点的值 成为大根堆(或大顶堆),父节点的值 小于或等于 孩子结点的值 成为小根堆(或小顶堆)
在顺序存储中:
-
满⾜:L(i)≥L(2i)且L(i)≥L(2i+1) (1 ≤ i ≤n/2 )—— ⼤根堆(⼤顶堆) -
满⾜:L(i)≤L(2i)且L(i)≤L(2i+1) (1 ≤ i ≤n/2 )—— ⼩根堆(⼩顶堆)
(注:顺序存储的二叉树中,i 的左孩子 2 i + 1 ,i 的右孩子 2 i + 2 ,i 的父节点 [ i / 2 ] ,i 的层次 [ log2n ] + 1)
算法思想(基于大根堆):通过将无序表转化为堆,可以直接找到表中最大值,然后将其提取出来(与待排序序列中的最后⼀个元素交换),令剩余的待排序元素序列再重建一个堆,取出次大值,如此反复执行就可以得到一个有序序列。
-
将无序序列构建成一个堆,根据升序降序需求选择大顶堆或小顶堆; -
将堆顶元素与末尾元素交换,将最大元素"沉"到数组末端; -
重新调整结构,使其满足堆定义,然后继续交换堆顶元素与当前末尾元素,反复执行调整+交换步骤,直到整个序列有序。
示意图:
代码实现:
//堆排序
//初始化大顶堆函数:void BuildHeap(int array[],int size)
//生成大顶堆函数:void Down(int array[],int i,int n)
//排序函数:void heapSort(int array[],int size)
//从小到大排序(结果就是大顶堆)
void Down(int *a,int i,int n){
int parent = i;
int child = 2 * i + 1;
while (child < n){
if (child + 1 < n && a[child] < a[child + 1])// 判断子节点那个大,大的与父节点比较
child++;
if(a[parent]<a[child]){ // 判断父节点是否小于子节点
swap(a[parent], a[child]); // 交换父节点和子节点
parent = child; // 子节点下标 赋给 父节点下标
}
child = child * 2 + 1; // 换行,比较下面的父节点和子节点
}
}
//建立大顶堆
void BuildHeap(int *a,int n){
for (int i = n / 2 - 1; i >= 0; i--){ // 倒数第二排开始, 创建大顶堆,必须从下往上比较
Down(a, i, n); // 否则有的不符合大顶堆定义
}
}
//堆排序
void HeapSort(int *a,int n){
BuildHeap(a, n);
for (int i = n - 1; i > 0; i--){
swap(a[0], a[i]);// 交换顶点和第 i 个数据
// 因为只有array[0]改变,其它都符合大顶堆的定义,所以可以从上往下重新建立
Down(a, 0, i); // 重新建立大顶堆
print(a, n);
}
}
输出:
8 6 7 3 5 0 4 1 2 9
7 6 4 3 5 0 2 1 8 9
6 5 4 3 1 0 2 7 8 9
5 3 4 2 1 0 6 7 8 9
4 3 0 2 1 5 6 7 8 9
3 2 0 1 4 5 6 7 8 9
2 1 0 3 4 5 6 7 8 9
1 0 2 3 4 5 6 7 8 9
0 1 2 3 4 5 6 7 8 9
算法效率分析:
空间复杂度:O(1)
时间复杂度:建堆 O( n ) ,排序 O(n log n),总的时间复杂度 O(n log n)。
算法稳定性:不稳定
(注:基于大顶堆的堆排序得到“递增序列”,基于小顶堆的堆排序得到“递减序列”。)
6.2 堆的插入删除
堆的结构:
const int maxn = 100;
int heap[maxn], n = 10;
插入元素:
对于⼩根堆,新元素放到表尾,与⽗节点对⽐, 若新元素⽐⽗节点更⼩,则将⼆者互换。新元素 就这样⼀路“上升”,直到⽆法继续上升为⽌。时间复杂度为 O(logn)。
每次“上升”调整只需要对比关键字1次
//插入元素(上升)
//对heap数组在[low,high]范围内进行向上调整
void UpAdjust(int low,int high){
int i = low, j = i * 2;//i为与调整节点,j为其左孩子。
while (j>=low)
{
if(heap[j]<heap[i]){
swap(heap[j], heap[i]);
i = j;
j = i / 2;
}else{
break;//孩子节点比与调整节点i小,调整结束。
}
}
}
//插入
void Insert(int x){
heap[++n] = x;
UpAdjust(1, n);
}
删除元素:
被删除的元素⽤堆底元素替代,然后让该元素不断“下坠”,直到⽆法下坠为⽌。时间复杂度为 O(logn)。
每次“下坠”调整可能需要对比关键字2次,也可能只需要对比1次。
//删除元素(下坠)
//对heap数组在[low,high]范围内进行向下调整
void DownAdjust(int low,int high){
int i = low, j = i * 2;//i为与调整节点,j为其左孩子。
while (j<=high)
{
if (j + 1 <= high && heap[j+1]>heap[j])
j = j + 1;
if(heap[j]>heap[i]){
swap(heap[j], heap[i]);
i = j;
j = i * 2;
}else{
break;//孩子节点比与调整节点i小,调整结束。
}
}
}
//删除
void DeleteTop(){
heap[1] = heap[n--];
DownAdjust(1, n);
}
7、归并排序
二路归并排序:
算法思想:先将所有的记录完全分开,然后两两合并,在合并的过程中将其排好序,最终能够得到一个完整的有序表。
归并过程中,每次得到的新的子表本身有序,所以最终得到的为有序表。
二路归并的核心在于如何将两个有序序列合并为一个有序序列。
示意图:
代码实现:
//将数组a的[l1,r1]和[l2,r2]区间合并
void merge(int *a, int l1, int r1, int l2, int r2){
int i = l1, j = l2;
int temp[maxn], index = 0;
while (i <= r1 && j <= r2)
{
if(a[i]<=a[j]){
temp[index++] = a[i++];
}
else{
temp[index++] = a[j++];
}
}
while(i<=r1)
temp[index++] = a[i++];
while(j<=r2)
temp[index++] = a[j++];
for (int i = 0; i < index; i++)
a[l1 + i] = temp[i];//将合并后的序列赋值回数组a
}
//递归实现
void MergeSort(int *a,int left,int right){
if (left<right)
{
int mid = (left + right) / 2;
MergeSort(a, left, mid);
MergeSort(a, mid + 1, right);
merge(a, left, mid, mid + 1, right);
}
}
//非递归实现
void MergeSort_2(int *a,int n){
for (int step = 2; step / 2 <= n; step *= 2)
{
for (int i = 0; i < n; i+=step)
{
int mid = i + step / 2 - 1;
if (mid + 1 <= n)
{
merge(a, i, mid, mid + 1, min(i + step - 1, n));
}
}
}
}
算法效率分析:
2路归并的“归并树”——形态上就是⼀棵倒⽴的⼆叉树,因此归并趟数为
空间复杂度:O(n)
时间复杂度:O(n log n)
算法稳定性:稳定
8、基数排序
算法思想:基数排序不同于之前所介绍的各类排序,前边介绍到的排序方法或多或少的是通过使用比较和移动记录来实现排序,而基数排序的实现不需要进行对关键字的比较,只需要对关键字进行“分配”与“收集”两种操作即可完成。
基数排序擅⻓解决的问题:
-
数据元素的关键字可以⽅便地拆分为 d 组,且 d 较⼩ -
每组关键字的取值范围不⼤,即 r 较⼩ -
数据元素个数 n 较⼤
示意图:
图示中是按照个位-十位-百位的顺序进行基数排序,此种方式是从最低位开始排序,所以被称为最低位优先法(简称“LSD法”)。
代码实现:
基数排序的代码实现在考研中不作要求,此处看看就行(代码来源于:csdn)
//基数排序
void RadixSort(int*a,int n)
{
int max=a[0];
int i;
for(int i=1;i<n;i++)//找出最大值
{
if(a[i]>max)
{
max=a[i];
}
}
int base=1;
int *b=(int*)malloc(sizeof(int)*n);//每次操作后元素的排序状态都会保存在这个中间过度数组中
while(max/base>0)//循环的次数为最大值的位数,比如520是三位数,则循环三次
{
int t[10]={0};//声明并定义一个“桶数组”
for(int i=0;i<n;i++)//实现上面第一个小图的功能
{
t[a[i]/base%10]++;
}
for(i=1;i<10;i++)//实现上面第二个小图的功能
{
t[i]+=t[i-1];
}
for(i=n-1;i>=0;i--)
{
b[t[a[i]/base%10]-1]=a[i];
t[a[i]/base%10]--;
}
for(i=0;i<n;i++)//赋值
{
a[i]=b[i];
}
base=base*10;
}
}
算法效率分析:
空间复杂度:O(r)
时间复杂度:O(d(n+r))
算法稳定性:稳定
⭐9、内部排序算法的优势分析
时间性能分析:
上表中的简单排序包含出希尔排序之外的所有插入排序,起泡排序和简单选择排序。同时表格中的 n 表示无序表中记录的数量;基数排序中的 d 表示进行分配和收集的次数。
-
在上表表示的所有“简单排序算法”中,以直接插入排序算法最为简单,当无序表中的记录数量 n 较小时,选择该算法为最佳排序方法。 -
所有的排序算法中单就平均时间性能上分析,快速排序算法最佳,其运行所需的时间最短,但其在最坏的情况下的时间性能不如堆排序和归并排序;堆排序和归并排序相比较,当无序表中记录的数量 n 较大时,归并排序所需时间比堆排序短,但是在运行过程中所需的辅助存储空间更多(以空间换时间)。 -
从基数排序的时间复杂度上分析,该算法最适用于对 n 值很大但是关键字较小的序列进行排序。 -
在所有基于“比较”实现的排序算法中(以上排序算法中除了基数排序,都是基于“比较”实现),其在最坏情况下能达到的最好的时间复杂度为 O(nlogn)
。
算法稳定性:
选择排序、快速排序和希尔排序都不是稳定的排序算法;而冒泡排序、插入排序、归并排序和基数排序都是稳定的排序算法。
(通过比较所有的排序算法,没有哪一种是绝对最优的,在使用时需要根据不同的实际情况适当选择合适的排序算法,甚至可以考虑将多种排序算法结合起来使用。)
以上是关于数据结构 | 考研八大内部排序算法的主要内容,如果未能解决你的问题,请参考以下文章