【数据结构】八大排序
- 1. 排序的概念和运用
- 1.1 概念
- 1.2 运用
- 2. 常规的排序算法介绍
- 一. 插入排序
- 1.1 直接插入排序
- 1.2 希尔排序
- 二. 选择排序
- 2.1 选择排序
- 2.2 堆排序
- 三. 交换排序
- 3.1 冒泡排序
- 3.2 快速排序
- 3.2.1 Hoare法
- 3.2.2 挖坑法
- 3.2.3 前后指针/左右指针法
- 3.2.4 分治法/递归法
- 3.2.5 非递归法
- 四. 归并排序
- 五.总结
1. 排序的概念和运用
1.1 概念
什么是排序:所谓排序,就是使一串记录,按照其中的某个或某些关键字的大小,递增或递减的排列起来的操作。
稳定性:假定在待排序的记录序列中,存在多个具有相同的关键字的记录,若经过排序,这些记录的相对次序保持不变,即在原序列中,r[i]=r[j],且r[i]在r[j]之前,而在排序后的序列中,r[i]仍在r[j]之前,则称这种排序算法是稳定的;否则称为不稳定的。
内部排序:数据元素全部放在内存中的排序。
外部排序:数据元素太多不能同时放在内存中,根据排序过程的要求不能在内外存之间移动数据的排序。
比较排序:通过比较两个元素的大小来确定元素在内存中的次序,我们常见的入选择排序、插入排序、比较排序与归并排序都属于比较排序。
非比较排序:通过确定每个元素之前,应该有多少个元素来排序,常见的非比较排序有基数排序、计数排序与桶排4序。
1.2 运用
排序在我们日常生活中运用十分常见,例如在京东买东西时,可以通过销量,评论数,新品,价格进行排序。
又比如世界五百强企业等,我们都可以运用排序。
2. 常规的排序算法介绍
// 插入排序
void InsertSort(int* a, int n);
// 希尔排序
void ShellSort(int* a, int n);
//选择排序
void SelectSort(int* a, int n);
// 堆排序
void AdjustDwon(int* a, int n, int root);
void HeapSort(int* a, int n);
// 冒泡排序
void BubbleSort(int* a, int n)
// 快速排序
void QuickSort(int* a, int left, int right);
// 归并排序
void MergeSort(int* a, int n)
一. 插入排序
1.1 直接插入排序
- 基本思想:
直接插入排序是一种简单的插入排序法。
其基本思想是:把待排序的记录按其关键码值的大小逐个插入到一个已经排好序的有序序列中,直到所有的记录插入完为止,得到一个新的有序序列 。
实际中我们玩扑克牌时,就用了插入排序的思想:
当插入第i(i>=1)个元素时,前面的array[0],array[1],…,array[i-1]已经排好序,此时用array[i]的排序码与array[i-1],array[i-2],…
的排序码顺序进行比较,找到插入位置即将array[i]插入,原来位置上的元素顺序后移。
void InsertSort(int* a, int n)
{
assert(a);//断言,不为空指针
for (int i = 0;i < n - 1;i++)
{
int end = i;
int x = a[end + 1];//记录最后一个元素,为要插入的目标元素
while (end >= 0)
{
//升序
//如果目标元素大于tmp中元素,则往后移
if (a[end] > x)
{
a[end + 1] = a[end];
end--;
}
else
{
break;
}
}
a[end + 1] = x;//将要插入的元素,插到不大于该数据的最后一个位置
}
}
直接插入排序的特性总结**:**
- 元素集合越接近有序,直接插入排序算法的时间效率越高。
- 时间复杂度:O(N^2)。
- 空间复杂度:O(1),它是一种稳定的排序算法。
- 稳定性:稳定。
1.2 希尔排序
基本思想:
希尔排序法又称缩小增量法。
希尔排序法的基本思想是:先选定一个整数,把待排序文件中所有记录分成个组,所有距离为的记录分在同一组内,并对每一组内的记录进行排序。然后,取,重复上述分组和排序的工作。当到达=1时,所有记录在统一组内排好序。
void ShellSort(int* a, int n)
{
int gap = n;
while (gap > 1)
{
gap /= 2;//确保最后一次的gap==1,直接插入排序
for (int i = 0;i < n - gap;i++)
{
int end = i;
int x = a[end + gap];
while (end >= 0)
{
if (a[end] > x)
{
a[end + gap] = a[end];
end -= gap;
}
else
{
break;
}
a[end + gap] = x;
}
}
}
}
希尔排序的特性总结:
- 希尔排序是对直接插入排序的优化。
- 当gap > 1时都是预排序,目的是让数组更接近于有序。当gap == 1时,数组已经接近有序的
了,这样就会很快。这样整体而言,可以达到优化的效果。我们实现后可以进行性能测试的
对比。 - 希尔排序的时间复杂度不好计算,需要进行推导,推导出来平均时间复杂度: O(N^1.3—
N^2)。 - 稳定性:不稳定。
二. 选择排序
基本思想:
每一次从待排序的数据元素中选出最小(或最大)的一个元素,存放在序列的起始位置,直到全部待排序的数据元素排完 。
2.1 选择排序
基本思想:
在元素集合array[i]–array[n-1]中选择关键码最大(小)的数据元素。
若它不是这组元素中的最后一个(第一个)元素,则将它与这组元素中的最后一个(第一个)元素交换。
在剩余的array[i]–array[n-2](array[i+1]–array[n-1])集合中,重复上述步骤,直到集合剩余1个元素。
void SelectSort(int* a, int n)
{
int left = 0;
int right = n - 1;//记录下标
while (left < right)
{
int min = left;
for (int i = left;i < right;i++)
{
if (a[i] < a[min])//比较,交换下标
{
min = i;//遍历找到最小值的下标,并记录
}
}
swap(&a[left], &a[min]);//交换
left++;
}
}
直接选择排序的特性总结:
- 直接选择排序思考非常好理解,但是效率不是很好。实际中很少使用
- 时间复杂度:O(N^2)
- 空间复杂度:O(1)
- 稳定性:不稳定
2.2 堆排序
基本思想:
原则: 先将原数组建成堆,需要注意的是排升序要建大堆,排降序建小堆。 注:以大堆为例:树中所以父亲结点值都 >= 孩子,根是最大的。
建堆: 一个根节点与子节点数据如果不符合大堆结构,那么则对根节点数据进行向下调整,而向下调整的前提是左右子树也符合大堆结构,所以从堆尾数据的根节点位置开始向下调整建大堆。
注:堆的两个特性:
结构性:用数组表示的完全二叉树。
有序性:任一结点的关键字是其子树所以结点是最大结点(或最小结点)排序: 大堆堆顶数据一定是待排数据中最大的,将堆顶数据与堆尾数据交换,交换后将除堆尾数据看成新堆,对现堆顶数据进行向下调整成大堆,以此循环直至排列完毕。
向下调整: 找到子节点中的较大数据节点比较,如果父节点数据比大子节点小则交换,直到不符合则停止向下交换,此时再次构成了一个大堆结构。
// 堆排序(升序)/建大堆
void HeapSort(int* a, int n)
{
int i;
//建大堆
//最后一个结点的1父亲下标:[(n-1)-1]/2;
for (i = (n - 1 - 1) / 2; i >= 0; i--)
{
Adjustdown(a, n, i);
}
for (i = n - 1; i >= 0; i--)
{
Swap(&a[0], &a[i]);//与当前堆尾数据交换
Adjustdown(a, i, 0);//对交换后堆顶数据进行向下调整
}
}
void Adjustdown(int* a, int n, int parent)
{
int child = parent * 2 + 1;
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 + 1;
}
else
{
break;
}
}
}
堆排序的特性总结:
- 堆排序使用堆来选数,效率就高了很多。
- 时间复杂度:O(N*logN)
- 空间复杂度:O(1)
- 稳定性:不稳定
三. 交换排序
基本思想:
所谓交换,就是根据序列中两个记录键值的比较结果来对换这两个记录在序列中的位置。
特点:将键值较大的记录向序列的尾部移动,键值较小的记录向序列的前部移动。
3.1 冒泡排序
基本思想:
每次遍历数组,对相邻的数据进行比较,大的元素往后放。
void BubbleSort(int* a, int n)
{
for (int i = 0;i < n-1;i++)//遍历数组,n个元素只需要n-1趟,每次比较,都可以将最大值放末尾
{
for (int j = 0;j < n - i - 1;j++)//n-1-i:因为每遍历一趟,可以确定一个元素,所以n-1-i;
{
if (a[j] > a[j + 1])
{
swap(&a[j],&a[j+1])
}
}
}
}
冒泡排序的特性总结:
- 冒泡排序是一种非常容易理解的排序
- 时间复杂度:O(N^2)
- 空间复杂度:O(1)
- 稳定性:稳定
3.2 快速排序
基本思想:
任取待排序元素序列中的某元素作为基准值,按照该排序码将待排序集合分割成两子序列。
左子序列中所有元素均小于基准值,右子序列中所有元素均大于基准值 然后最左右子序列重复该过程,直到所有元素都排列在相应位置上为止。
3.2.1 Hoare法
int PartSort1(int* a, int left, int right)
{
int keyi = left; //取最左边的元素做key
while (left < right)
{
//右先走,找小
//left<right:避免left和right错过或者越界
//=:避免死循环
while (left < right && a[right] >= a[keyi])
{
right--;
}
//左后走,找大
while (left < right && a[left] <= a[keyi])
{
left++;
}
Swap(&a[left], &a[right]);
}
//记录二者相遇的位置并让其与key交换
int meet = left;
Swap(&a[meet], &a[keyi]);
return meet;//返回相遇时下标值
}
3.2.2 挖坑法
int PartSort2(int* a, int left, int right)
{
int mid = GetMidIndex(a, left, right);
Swap(&a[mid], &a[left]);//使得中间值永远在最左,便于决定谁先走
int key = a[left];//保存key值
int pivot = left;//保存坑下标
while (left < right)
{
//右边先找
while (left<right && a[right]>=key)
{
right--;
}
//填坑
a[pivot] = a[right];
pivot = right;
//再从左边找
while (left < right && a[left] <= key)
{
left++;
}
//填坑
a[pivot] = a[left];
pivot = left;
}
a[pivot] = key;
return pivot;
}
3.2.3 前后指针/左右指针法
int PartSort3(int* a, int left, int right)
{
int mid = GetMidIndex(a, left, right);
Swap(&a[mid], &a[left]);
//初始化前后指针
int cur = left+1, prev = left;
while (cur < right)
{
if(a[cur]<a[left] )//找到比基准值小的
Swap(&a[++prev], &a[cur]);
cur++;
}
Swap(&a[prev], &a[left]);//遍历结束将基准值放在定位点
return prev;
}
3.2.4 分治法/递归法
在了解快排的思想之后,我们会发现快速排序的排序过程是这样的:
先选定一个 key 做基准值,经过单趟排序后 key 左边位置的元素都小于 key,右边位置的元素都大于 key,这就使得 key 的位置被最终确定,即我们一趟排序可以确定一个元素的位置。
现在我们只需要对 key 的左区间和右区间再进行单趟排序即可;key 的左区间经过单趟排序之后又会确定一个元素的位置,然后再对该元素的左右区间进行单趟排序,直到 key 的左右区间只有一个元素 (一个元素本身就有序,不需要再进行排序) 或者不存在左右区间时说明排序完成。
左右区间又可以被划分为左右区间,即不断被划分为子问题,这就是递归的思想。
// 快速排序递归实现
void QuickSort(int* a, int left, int right)
{
//如果左右区间相等或者右区间小于左区间直接返回
if (right <= left)
return;
//单趟排序 -- 确定单个元素的位置
int keyi = PartSort1(a, left, right);
//递归左区间
QuickSort(a, left, keyi - 1);
//递归右区间
QuickSort(a, keyi + 1, right);
}
3.2.5 非递归法
void QuickSortNonR(int* a, int left, int right)
{
//首先构建一个栈(C语言来说需要自己实现)
ST st;
StackInit(&st);
StackPush(&st, left);//将左右区间入栈
StackPush(&st, right);
while (!StackEmpty(&st))
{
int end = StackTop(&st);//读取区间数据
StackPop(&st);
int begin = StackTop(&st);
StackPop(&st);
int mid = PartSort3(a, begin, end);//排序(排好基准值)
//划分基准值的左右区间
int begin1 = mid + 1, end1 = end;
//先入右边区域(栈的特点是先入后出)
if (end1 - begin1 + 1 > 1)
{
StackPush(&st, begin1);
StackPush(&st, end1);
}
//再将左边区域入栈
int begin2 = begin, end2 = mid-1;
if (end2 - begin2 + 1 > 1)
{
StackPush(&st, begin2);
StackPush(&st, end2);
}
}
//到空栈则排序结束
StackDestroy(&st);//栈销毁
}
快速排序的特性总结:
- 快速排序整体的综合性能和使用场景都是比较好的,所以才敢叫快速排序。
- 时间复杂度:O(N*logN)。
-
- 空间复杂度:O(logN)。
- 稳定性:不稳定。
四. 归并排序
基本思想:
归并排序(MERGE-SORT)是建立在归并操作上的一种有效的排序算法,该算法是采用分治法 (Divide and
Conquer)的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为二路归并。
void _MergeSort(int* a, int left, int right, int* tmp)
{
//当区间大小为1时,无左右区间,直接返回
if (left >= right)
return;
int mid = left + (right - left) / 2;
//递归左区间有序
_MergeSort(a, left, mid, tmp);
//递归右区间有序
_MergeSort(a, mid + 1, right, tmp);
//当左右区间都有序时开始归并 -- 取小的尾插
int left1 = left, right1 = mid; //左区间
int left2 = mid + 1, right2 = right; //右区间
//在tmp数组中的相应位置尾插
int i = left;
while (left1 <= right1 && left2 <= right2)
{
if (a[left1] <= a[left2])
{
tmp[i++] = a[left1++];
}
else
{
tmp[i++] = a[left2++];
}
}
//将较大的数组链接到tmp后面
while (left1 <= right1)
{
tmp[i++] = a[left1++];
}
while (left2 <= right2)
{
tmp[i++] = a[left2++];
}
//将tmp中已归并的数据拷贝到原数组的相应区间上
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("malloc fail\n");
exit(-1);
}
//归并排序
_MergeSort(a, 0, n - 1, tmp);
//销毁
free(tmp);
tmp = NULL;
}
归并排序的特性总结:
- 归并的缺点在于需要O(N)的空间复杂度,归并排序的思考更多的是解决在磁盘中的外排序问题。
- 时间复杂度:O(N*logN)
- 空间复杂度:O(N)
- 稳定性:稳定
五.总结
💓 感谢阅读!