- 排序:什么是排序?排序就是使一串记录,按照其中的某个或某些关键字的大小,递增或递减的排列起来的操作
- 内部排序:数据元素全部放在内存中的排序
- 外部排序:数据元素太多不能同时放在内存中,根据排序过程的要求不能在内外存之间移动数据的排序
- 稳定性:假定在待排序的记录序列中,存在多个具有相同的关键字的记录,若经过排序,这些记录的相对次序保持不变,即在原序列中,r[i]=r[j],且r[i]在r[j]之前,而在排序后的序列中,r[i]仍在r[j]之前,则称这种排序算法是稳定的;否则称为不稳定的
- 比如:京东上面的综合排序和价格排序
- 比如:高校之间热度的排序
- 还有我常见的"点外卖",我们一般都会点热度最高的店铺,还有就是我们在学校中考试成绩的排序等等…
- 直接插入排序其基本思想是:
把待排序的记录按其关键码值的大小逐个插入到一个已经排好序的有序序列中,直到所有的记录插入完为止,得到一个新的有序序列
- 实际中我们玩扑克牌时,就用了插入排序的思想…
直接插入排序(单趟)过程分析:
代码实现:
void InsertSort(int* p, int n)
{
for (int i = 0; i < n - 1; ++i)
{
int end = i;
//保存end下标后面的数据
int tmp = p[end + 1];
//当end为
while (end >= 0)
{
//升序,判断tmp是否小于p[end]
if (tmp < p[end])
{
//小于则覆盖end后面的数据
p[end + 1] = p[end];
--end; //end往前移
}
else
{
break;
}
}
//当end走到-1时,放在循环里面处理会导致数组越界访问
p[end + 1] = tmp;
}
}
直接插入排序的特性总结:
希尔排序:希尔排序又称"缩小增量法"。
希尔排序的基本思想是:
预排序:每次间隔为gap,直到i < 0时,才进行下一轮的预排序。每次进行间隔gap / 3 + 1的预排。当gap为1时,说明数据已经接近有序,直接进行插入排序
代码实现:
void ShellSort(int* p, int n)
{
int gap = n;
while (gap > 1)
{
//比如有1000个数,gap之间的间隔为"334",第二次间隔为112次,第三次间隔为"38",第四次为"13",第五次为"5", 第六次为"2"(前面为"预排序"),最后为"1"进行(插入排序)
//当gap > 1时,进行预排序-----当gap等于1时,说明已经接近有序,进行"插入排序"
//一开始进行排序时,i必须小于n - gap,如果i < n时,会导致tmp赋值时,会导致越界
for (int i = 0; i < n - gap; ++i)
{
int end = i;
int tmp = p[end + gap];
while (end >= 0)
{
if (tmp < p[end])
{
p[end + gap] = p[end];
end -= gap;
}
else
{
break;
}
}
p[end + gap] = tmp;
}
}
}
注意:gap每次走几步是没有规定的,可以每次走gap/2步,甚至是每次走gap/5+1步,控制好最后gap为1就行
希尔排序的特性总结:
基本思想:每一次从待排序的数据元素中选出最小(或最大)的一个元素,存放在序列的起始位置,直到全部待排序的数据元素排完
选择排序过程解析:
void SelectSort(int* p, int n)
{
//找数组中最大值和最小值,然后交换到最左边和最右边里面
int left = 0;
int right = n - 1;
while (left < right)
{
int Max = right, Min = left;
for (int i = left; i <= right; ++i)
{
//在左闭右闭[left, right]区间中找最大值和最小值
if (p[i] > p[Max])
Max = i;
if (p[i] < p[Min])
Min = i;
}
//交换数据
Swap(&p[left], &p[Min]);
//如果left和Max重叠,需修正Max
if (left == Max)
Max = Min;
Swap(&p[right], &p[Max]);
//更新left和right
++left;
--right;
}
}
Ps:上面代码是优化版的"选择排序",同时找最大值和最小值
直接选择排序的特性总结:
- 堆排序即利用堆的思想来进行排序,总共分为两个步骤:
1.1、建堆
- 升序,建大堆
- 降序,建小堆
- 利用堆删除思想来进行排序
- 升序,建大堆
ps:不管是建大堆还是小堆,最好使用向下调整算法(时间复杂度:O(N)),因为它比向上调整算法(时间复杂度(O(nlongN))建堆更优
void AdJustDown(HPDataType* a, size_t root, size_t size)
{
size_t parent = root;
size_t child = (parent * 2) + 1;
//孩子节点大于最大节点时,就说明向下调整完毕,退出循环
while (child < size)
{
//选出左右孩子中最小/大的那个->"小/大堆"
//如果右孩子(a[child + 1])小于左孩子(a[child])
//则最小的孩子是右孩子,自增一下左孩子下标就是右孩子了
if (a[child + 1] > a[child] && child + 1 < size) //大堆
//if (a[child + 1] < a[child] && child + 1 < size) //小堆
{
++child;
}
//当孩子节点小于/大于父节点时进行调整,说明这是一个"小堆/大堆"->根节点存储最小/最大的节点
//if (a[child] < a[parent]) //小堆
if (a[child] > a[parent]) //大堆
{
//交换节点数据,并且继续往下调整
Swap(&a[child], &a[parent]);
//更新父节点下标和孩子节点下标
parent = child;
child = (parent * 2) + 1;
}
else
{
break;
}
}
}
void HeapSort(int* a, size_t n)
{
//排升序,建"大堆"(调用向下调整函数接口)
for(int i = (n - 1 - 1) / 2; i >= 0; --i)
{
AdJustDown(a, i, n);
}
}
直接选择排序的特性总结:
基本思想:
- 所谓交换,就是根据序列中两个记录键值的比较结果来对换这两个记录在序列中的位置
- 交换排序的特点是:将键值较大的记录向序列的尾部移动,键值较小的记录向序列的前部移动
void BubbleSort(int* p, int n)
{
//冒泡排序:如果进行升序排序,p[i]大于p[i + 1]时进行交换,大的往后移...
for (int i = 0; i < n; ++i)
{
int exchage = 0;
//第一次走n趟,第二次走n - 1,第三次n - 2.....则判断可以写成n - i
for (int j = 1; j < n - i; ++j)
{
if (p[j - 1] > p[j])
{
exchage = 1;
Swap(&p[j - 1], &p[j]);
}
}
if (exchage == 0)
break;
}
}
冒泡排序的特性总结:
快速排序:
// 假设按照升序对array数组中[left, right)区间中的元素进行排序
void QuickSort(int array[], int left, int right)
{
if(right - left <= 1)
return;
// 按照基准值对array数组的 [left, right)区间中的元素进行划分
int key = Partion(array, left, right);
// 划分成功后以div为边界形成了左右两部分 [left, key) 和 key+1, right)
// 递归排[left, key)
QuickSort(array, left, key);
// 递归排[key+1, right)
QuickSort(array, key+1, right);
}
上述为快速排序递归实现的主框架,发现与二叉树前序遍历规则非常像,在写递归框架时可想想二叉树前序遍历规则即可快速写出来,后序只需分析如何按照基准值来对区间中数据进行划分的方式即可
将区间按照基准值划分为左右两半部分的常见方式有:
int PartSort(int* p, int left, int right)
{
//"优化"后面会讲到
int mid = GetMidIndex(p, left, right);
Swap(&p[mid], &p[left]);
int key = left;
while (left < right)
{
//key在最左边,先走right,找比key小的值
while (p[key] <= p[right] && left < right)
--right;
//right向后走找到比key小的值后,left往前走找比key大的值
while (p[key] >= p[left] && left < right)
++left;
//当left走到大于key并且right走到小于key的值时,进行交换
Swap(&p[left], &p[right]);
}
//当left和right相遇时,交换key和它们其中一个的值
Swap(&p[key], &p[left]);
return left;
}
代码实现:
int PartSort(int* p, int left, int right)
{
//"优化"后面会讲到
int mid = GetMidIndex(p, left, right);
Swap(&p[mid], &p[left]);
//首先保存最左边的值
int key = p[left];
//建坑位
int pit = left;
while (left < right)
{
//首先从右边开始走,找比key小的数据
while (key <= p[right] && left < right)
{
--right;
}
//覆盖坑位数据
p[pit] = p[right];
//更新新的坑位
pit = right;
//右边找完完后,走左边,左边找比key大的值的
while (key >= p[right] && left < right)
{
++left;
}
//覆盖坑位数据
p[pit] = p[left];
//更新新的坑位
pit = left;
}
//出了循环后说明left和right相遇了,只有一个坑位
//把key的数据给到pit
p[pit] = key;
return pit;
}
挖坑法的优势:
代码实现:
int PartSort(int* p, int left, int right)
{
int prev = left;
int cur = left + 1;
int key = p[left];
//当cur走到尾时,退出循环
while (cur <= right)
{
//当p[cur]小于key并且p[++prev]不等于p[cur]时,交换数据,等于就直接跳过
if (p[cur] < key && p[++prev] != p[cur])
Swap(&p[prev], &p[cur]);
//自增cur
++cur;
}
//cur走到尾时,prev位置的值一定比key小,交换key位置和prev的内容
Swap(&p[prev], &p[left]);
return prev;
}
非递归版本
思想:使用"栈"模拟快速排序中的递归
代码实现:
void QuickSort3(int* p, int begin, int end)
{
ST st; //声明栈
StackInit(&st); //初始化栈
//将begin和end入栈,待排序时需要使用
StackPush(&st, begin);
StackPush(&st, end);
while (!StackEmpty(&st))
{
//栈:"先进后出",栈顶的值是end,将它赋值给对应的right,然后出栈
int right = StackTop(&st);
StackPop(&st);
//将栈中的begin赋给left,然后出栈
int left = StackTop(&st);
StackPop(&st);
//使用"前后指针版本"进行排序
int key = PartSort3(p, left, right); //进行排序
//排序后:[left, key - 1] key [key + 1, right]
//key左区间:[left, key - 1]
if (left < key - 1)
{
StackPush(&st, left);
StackPush(&st, key - 1);
}
//key右区间:[key + 1, right]
//当key+1大于right时说明key不在区间中[0+1, 0]----当key+1等于right时说明只有一个值[0, 0]
if (key + 1 < right)
{
StackPush(&st, key + 1);
StackPush(&st, right);
}
//这里是先进行key右区间排序,因为栈的性质("先进后出")
}
StackDestory(&st);
}
Ps:这里省略了造轮子,栈的实现过于简单…
快速排序优化
当快速排序遇到"接近升序的元素集合"时,如果集合量很大,每次递归进行排序找的left都是最小的,通过等差数列(n-1+n-2+…+0)得出时间复杂度为:O(N2),当数据量越大时,会导致"栈溢出"(递归太深,无法释放),这里我们需要进行优化.
"三数取中"优化
代码实现:
int GetMidIndex(int* p, int left, int right)
{
int mid = left + (right - left) / 2;
if (p[mid] > p[left])
{
if (p[mid] < p[right])
{
return mid;
}
else if (p[left] > p[right])
{
return left;
}
else
{
return right;
}
}
//p[mid] < p[left]
else
{
if (p[mid] > p[right])
{
return mid;
}
else if (p[left] < p[right])
{
return left;
}
else
{
return right;
}
}
}
“小区间优化”
当递归层数越来越深的时候,递归所消耗的次数会越来越高,效率也会有一定的消耗,这是在Debug下编译的情况,但是在"realese"下递归会被优化的很好,不使用"小区间优化"
思想:给定一个整数值,当递归层数大于这个值时,调用"直接插入排序算法"进行排序
代码实现:
void QuickSort(int array[], int left, int right)
{
if(left >= right)
return;
//"小区间优化"
if(right - left + 1 >= 10)
{
InsertSort(array + left, right - left + 1);
}
else
{
int key = Partion(array, left, right);
QuickSort(array, left, key);
QuickSort(array, key+1, right);
}
}
快速排序的特性总结:
思想:
代码实现:
void _MergeSort(int* p, int begin, int end, int* tmp)
{
//begin等于end时说明区间为[0, 0]只有一个值,begin大于end时说明区间不存在[1, 0](mid为0时)
if (begin >= end)
return;
int mid = begin + (end - begin) / 2;
//分割数组--->分成二个区间 [begin, mid] [mid+1, end]
_MergeSort(p, begin, mid, tmp);
_MergeSort(p, mid + 1, end, tmp);
//归并分割后的数据
int begin1 = begin, end1 = mid;
int begin2 = mid + 1, end2 = end;
int index = begin;
//使用二叉树中“后序遍历的方法"进行排序并且"归并到原数组" --- 跟合并二个数组和链表相似-> 取小进行尾插(升序)
while (begin1 <= end1 && begin2 <= end2)
{
//取小放到辅助数组
if (p[begin1] < p[begin2])
{
tmp[index++] = p[begin1++];
}
else
{
tmp[index++] = p[begin2++];
}
}
//二个区间中可能还会存在数据没有入到辅助数组tmp的情况
while (begin1 <= end1)
tmp[index++] = p[begin1++];
while (begin2 <= end2)
tmp[index++] = p[begin2++];
//将排序好的辅数组(tmp)拷贝到原数组(p)中
memcpy(p + begin, tmp + begin, sizeof(int) * (end - begin + 1));
}
void MergeSort(int* p, int n)
{
int* tmp = (int*)malloc(sizeof(int) * n);
_MergeSort(p, 0, n - 1, tmp);
}
非递归版本
思想:使用"循环"模拟递归
void MergeSortNonR(int* p, int n)
{
int* tmp = (int*)malloc(sizeof(int) * n);
assert(tmp);
//一开始gap为1,两两进行归并
int gap = 1;
//gap等于或大于n说明已经合并完成
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 = i;
//判断边界问题
if (end1 >= n)
end1 = n - 1;
//当"end2越界时,修正end2"
if (end2 >= n)
end2 = n - 1;
//当b"begin2未越界且end2越界时,修正end2"
if (begin2 < n && end2 >= n)
end2 = n - 1;
//当"begin2和end2都越界时",说明是一个不存在的区间,修正为不进循环的区间
if (begin2 >= n && end2 >= n)
{
begin2 = n;
end2 = n - 1;
}
while (begin1 <= end1 && begin2 <= end2)
{
if (p[begin1] < p[begin2])
{
tmp[index++] = p[begin1++];
}
else
{
tmp[index++] = p[begin2++];
}
}
while (begin1 <= end1)
tmp[index++] = p[begin1++];
while (begin2 <= end2)
tmp[index++] = p[begin2++];
}
//将归并好的辅助数组(tmp)拷贝回原数组中
memcpy(p, tmp, sizeof(int) * n);
//每次归并的间隔为2的倍数,第一次gap为1,第二次为2,第三次为4......
gap *= 2;
}
}
归并排序的特性总结:
排序方法 | 平均情况 | 最好情况 | 最坏情况 | 辅助空间 | 稳定性 |
---|---|---|---|---|---|
冒泡排序 | O(N2) | O(N) | O(N2) | O(1) | 稳定 |
简单选择排序 | O(N2) | O(N2) | O(N2) | O(1) | 不稳定 |
直接插入排序 | O(N2) | O(N) | O(N2) | O(1) | 稳定 |
希尔排序 | O(NlongN— O(N2)) | O(N1.3) | O(N2) | O(1) | 不稳定 |
堆排序 | O(NlongN) | O(NlongN) | O(NlongN) | O(N) | 稳定 |
归并排序 | O(NlongN) | O(NlongN) | O(NlongN) | O(N) | 稳定 |
快速排序 | O(NlongN) | O(NlongN) | O(N2) | O(longN— O(N)) | 不稳定 |