此处排序均是以排升序为例
1.插入排序
其思想与玩扑克牌时将一张牌插入哪里时相似:把待排序的记录按其关键码值的大小逐个插入到一个已经排好序的有序序列中,直到所有的记录插入完为止,得到一个新的有序序列 。
1.直接插入排序
一趟排序的场景,也就是确定一个数的位置:对一个已经有序的数组,插入一个数。
分析一趟排序:
最后一个数下标为end,要插入的数是后一位,下标为end+1,如果end位比end+1位大,Swap,然后end向前走一步。否则break。比如第一趟就是,有一个数(一个数可以视为有序),插入第二个数,end=0;
那么对一个数组排序就是,end从0到n-2共n-1趟
//插排
void InsertSort(int* a, int n)
{
int end = 0;
int i = 0;
for (i = 0; i < n - 1; i++)
{
end = i;
//一趟
while (end >= 0)
{
if (a[end] > a[end + 1])
{
Swap(&a[end], &a[end + 1]);
end--;
}
else
{
break;
}
}
}
}
直接插入排序的特性总结:
1. 元素集合越接近有序,直接插入排序算法的时间效率越高
2. 时间复杂度:O(N^2),最好的情况是数组已经有序,复杂度为O(N)
3. 空间复杂度:O(1),它是一种稳定的排序算法
4. 稳定性:稳定
2.希尔(Shell)排序
希尔排序是对直接插入排序的优化:通过预排序,将数组尽可能的有序。
大致思路:选一个整数gap,将距离为gap的分为一组,这样就可以分为gap组,比如gap=5
然后对每组进行排序,排序后缩小gap的值,再对每组进行排序,这个过程称为预排序。
当gap=1,这个数组已经十分接近有序,用直接插入排序的方法复杂度接近于O(N)
void ShellSort(int* a, int n)
{
int gap = n;
int i = 0;
int end = 0;
while (gap > 1)
{
gap = gap/ 3 + 1;//大多数Shell用gap/3来分隔区间,确保最后一趟gap =1 要+1
for (i = 0; i < n - gap; i++)
{
end = i;
while (end >= 0)
{
if (a[end] > a[end + gap])
{
Swap(&a[end], &a[end + gap]);
end -= gap;
}
else
{
break;
}
}
}
}
}
关于Shell的复杂度,由gap影响,很复杂,这里直接记住,最好的情况是O(n^1.3)。
关于他的稳定性,并不稳定。关于稳定性是什么,后面会做比较。
2.选择排序
选择排序的思想就是选择最大或最小:每一次从待排序的数据元素中选出最小(或最大)的一个元素,存放在序列的起始位置,直到全部待排序的数据元素排完 。
1.直接选择排序
一趟遍历:选出一个最大的放右边,选出一个最小的放左边。此时,这个数组里面最大和最小已经到了对应位置。
然后控制区间,再排剩下的。
第一趟:
控制区间,此时 排[begin+1,end-1]
void SelectSort(int* a, int n)
{
int begin = 0;
int end = n - 1;
while (begin < end)
{
int maxi = begin;
int mini = begin;
for (int i = begin+1; i <= end; i++)
{
if (a[i] > a[maxi])
{
maxi = i;
}
if (a[i] < a[mini])
{
mini = i;
}
}
Swap(&a[begin], &a[mini]);
if (begin == maxi)//如果maxi和begin下标重合,修正一下maxi的下标
{
maxi = mini;
}
Swap(&a[end], &a[maxi]);
begin++;
end--;
}
}
2.堆排
堆排也是一种选择排序。
1.堆排无论处理什么样的数据,都是O(N*LogN)的时间复杂度。
2.排升序建大堆,排降序建小堆。
3.O(1)的空间复杂度。
4.不稳定。
详见另一篇文章——堆。
3.交换排序
交换排序,即交换的思想。我们最开始接触的冒泡排序就是这种思想,但冒泡实在很拉。
1.冒泡
void BubbleSort(int* a, int n)
{
for (int j = 0; j < n; j++)
{
int flag = 0;
for (int i = 1; i < n-j; i++)
{
if (a[i-1] > a[i])
{
Swap(&a[i], &a[i -1]);
flag = 1;
}
}
if (flag == 0)
break;
}
}
1. 时间复杂度:O(N^2) ,最好情况是O(N)
2. 空间复杂度:O(1)
3. 稳定性:稳定
2.快排
快排呢,时间复杂度是O(N*LogN),是用递归的思想来排序,也有为了排大量数据的非递归版本。
先讲讲是怎么排序的
选择左边的位置为Key
R从右边走,R先走,找比key小的
L从左边走,找比key大的
找到了,Swap一下
继续 ,直到L = R
然后把key值放L = R的位置。把相遇位置的下标给key
这就是发明快排的大佬Hoare写的一趟排序。他所带来的结果是:
1.key最开始代表的值已经到了合适的位置,就是排好序的位置。
2.key左边的数都小于key,key右边的数都大于key。
然后解释一些不理解的地方:
为什么相遇的位置就可以和key交换,一定可以确保他两相遇的位置的值比key小吗?
是的。原因是右边R先走。一般选key为左边,这样就右边先走。
右边找小,如果是右边去和L相遇,L的位置是小的吧,因为L在找大,找大就会交换,所以L所在位置一定是比key小的。
如果是左边去和R相遇,R找到小停下来,L去找Rt停下来,这样停下来的地方就是比key小的。
绝妙!
key已经到了合适的位置,接下来就是把左边区间再排序,右边区间再排序。整体就是
排key,排key左,
排key,排key左,
……
返回
排key右
……
int PartSort1(int* a, int left, int right)
{
int mid = GetMiddle(a, left, right);
Swap(&a[mid], &a[left]);//三数取中
int key = left;
while (left < right)
{
while (left < right && a[right] >= a[key])
{
--right;
}
while (left < right && a[left] <= a[key])
{
++left;
}
Swap(&a[left], &a[right]);
}
Swap(&a[key], &a[left]);
return left;
}
void QuickSort1(int* a, int begin, int end)
{
//区间只剩一个数,或者不存在的区间就返回
if (end <= begin)
{
return;
}
int keyi = PartSort3(a, begin, end);
//确定了keyi的位置,递归左边和右边[begin,key-1] key[key+1,end]
QuickSort1(a, begin, keyi - 1);
QuickSort1(a, keyi + 1, end);
}
优化的快排加入了三数取中,和小区间递归优化:
//三数取中
int GetMiddle(int* a, int left, int right)
{
int mid = (left + right) / 2;
if (a[mid] > a[left])
{
if (a[left] > a[right])
{
return left;
}
else if (a[mid] > a[right])
{
return right;
}
else
{
return mid;
}
}
else
{
if (a[mid] > a[right])
{
return mid;
}
else if (a[left] > a[right])
{
return right;
}
else
{
return left;
}
}
}
void QuickSort2(int* a, int begin, int end)
{
//区间只剩一个数,或者不存在的区间就返回
if (end <= begin)
{
return;
}
if (end - begin + 1 > 10)
{
int keyi = PartSort3(a, begin, end);
//确定了keyi的位置,递归左边和右边[begin,key-1] key[key+1,end]
QuickSort2(a, begin, keyi - 1);
QuickSort2(a, keyi + 1, end);
}
else
{
InsertSort(a + begin, end - begin + 1);
}
}
挖坑法
选左边为key,
左边形成坑,
R开始走,找小,填坑,自己形成坑位
L接着走,找大,填坑,自己形成坑
int PartSort2(int* a, int left, int right)
{
int mid = GetMiddle(a, left, right);
Swap(&a[mid], &a[left]);
int key = a[left];
int hole = left;
while (left < right)
{
while (a[right] >= key && left < right)
{
right--;
}
a[hole] = a[right];
hole = right;
while (a[left] <= key && left < right)
{
left++;
}
a[hole] = a[left];
hole = left;
}
a[hole] = key;
return hole;
}
前后指针法
cur找小,找到小,++prev,Swap
int PartSort3(int* a, int left, int right)
{
int mid = GetMiddle(a, left, right);
Swap(&a[mid], &a[left]);
int key = left;
int prev = left, cur = left+1;
while (cur <= right)
{
if (a[cur] < a[key] && ++prev != cur)
{
Swap(&a[prev], &a[cur]);
}
cur++;
}
Swap(&a[prev], &a[key]);
return prev;
}
非递归快排(重要)
递归是有消耗的,况且栈的空间十分有限,所以,递归改非递归就很有必要辣!
一般改成循环,但有的改不了循环,需要借助栈!放堆空间上,堆空间很大,我们可以狠狠的利用。就是用栈的特性模拟递归。
先选出key,再把key的右区间存进去,再存左区间
再把左拿出来,排好key,存右存左,到栈为空。
void QuickSortNonR(int* a, int begin, int end)
{
SqStack st;
StackInit(&st);
StackPush(&st, end);
StackPush(&st, begin);
while (!StackEmpty(&st))
{
begin = StackTop(&st);
StackPop(&st);
end = StackTop(&st);
StackPop(&st);
int keyi = PartSort3(a, begin,end);
if (keyi + 1 < end)
{
StackPush(&st, end);
StackPush(&st, keyi + 1);
}
if(begin < keyi - 1)
{
StackPush(&st, keyi - 1);
StackPush(&st, begin);
}
}
StackDestory(&st);
}