同时 还有两个比较基础的概念:
插入排序的思想十分的简单:
把待排序的元素插入到一个 有序序列 的 正确的位置,直到所有的记录插入完全为止,得到一个新的有序序列。
其实我们在玩扑克牌的时候就是这个思想
为了能更好的理解插入排序,我们从他的一次循环看起:(以升序为例)
循环的基本思路是: 如果a[end]比temp(待插入的数)要小,就说明还要继续往下寻找,这是执行a[end+1]=a[end]
,并将end- -,循环的条件时end >=0
直到找到比 temp要小的数 或者 end==0 时还没有找到,这时就要跳出循环执行:a[end+1]=temp
注意: 这里如果end==0
时还没有找到的时候,出循环的时候end为-1,但是a[end+1]
实际上还是第一个元素
这是直接插入排序的一次单次循环,实际上我们发现直接插入排序一个重要的前提是要有一个有序的数组。
在排序的最开始 我们可以认定第一个数为一个大小为1的有序数组,来启动上述介绍的单次循环,循环结束我们就得到了一个前两个元素有序的数组 …直到前n-1个元素(数组大小为n)有序,将最后一个元素插入,这是最后一次循环。 所以我们还要在上面单次循环的基础上在加一个循环来完成排序
代码
void InsertSort(int* a, int n)
{
for (int i = 0; i < n - 1; i++)
{
int end = i;
int temp = a[i + 1];
while (end >= 0)
{
if (a[end] > temp)
{
a[end + 1] = a[end];
end--;
}
else
break;
}
a[end + 1] = temp;
}
}
总结
O(N^2)
O(1)
希尔排序实际上是在插入排序的基础上做出了一些改进,希尔排序又叫缩小增量法。
其基本思想是:
将数组按一定间隔分成若干个组,对每个组里面的元素进行直接选择排序,然后缩小间隔,在对重新分的组挨个进行选择排序…直到最后间隔为1(实际上直接插入排序是间隔为1的希尔排序)
代码
void ShellSort(int* a, int n)
{
int gap = n ;
while (gap > 1)
{
gap = gap / 3 + 1; //注意这里是以三倍的速度缩小gap,这里+1是因为要保证最后一次gap为1
for (int i = 0; i < n -gap; i++)
{
int end = i;
int temp = a[i + gap];
while (end >= 0)
{
if (a[end] > temp)
{
a[end + gap] = a[end];
end -= gap;
}
else
break;
}
a[end + gap] = temp;
}
}
}
总结
O(N^1.25)
这是通过大量实验得到的经验公式O(1)
基本思路:
每次遍历整个数组从数组中选择出最大的数(或最小的数),最放到数组的末尾(或数组的首尾),直到全部待排序的数据元素排完
我们以一个数组为例:
void SelectSort(int* a, int n)
{
int begin = 0;
int end = n - 1;
while (begin < end)
{
int min = begin;
int max = end;
for (int i = begin+1; i < end;i++)
{
if (a[i] > a[max])
max = i;
if (a[i] < a[min])
min = i;
}
swap(&a[begin], &a[min]);
if (begin != max)
swap(&a[end], &a[max]);
else
swap(&a[end], &a[min]);
begin++;
end--;
}
}
总结:
O(N^2)
O(1)
可以参考一下以前的博客:数据结构:堆 的详解
代码:
void AjustDown(int* a, int n, int parents)
{
int child = 2 * parents + 1;
while (child < n)
{
if (child + 1 < n && a[child + 1] > a[child])
child++;
if (a[child] > a[parents])
swap(&a[child], &a[parents]);
parents = child;
child = 2 * parents + 1;
}
}
void HeapSort(int* a, int n)
{
for (int i = n/2-1 ; i >= 0; i--)
{
AjustDown(a,n,i);
}
for (int i = n-1; i >0 ; i--)
{
swap(&a[0], &a[i]);
AjustDown(a, i, 0);
}
}
总结:
O(N*logN)
O(1)
基本思想: 就是根据序列中两个记录键值的比较结果来对换这两个记录在序列中的位置,在视觉上的效果像:较大键值的记录向序列的尾部移动,键值较小的记录向序列的前部移动。
[0,n-2]
,也就是n-1次比较。
看完一次循环大家或许就理解了冒泡排序名字的由来,最大的数像气泡一样向上移动,在往下思考这次遍历过后,实际上最后一个已经是有序的了,所以还要进行n-1次这样的遍历,所以时间复杂的是O((n-1)+(n-2)+(n-3)+(n-4).....+0)=O(n^2)
代码
void BubbleSort(int* a, int n)
{
for (int i = 0; i < n - 1; i++)
{
int k = 0;
for (int j = 0; j < n - 1 - i; j++) //每一次遍历结束,下一次要遍历的个数都会减少一个
{
if (a[j] > a[j + 1])
{
int temp = a[j];
a[j] = a[j + 1];
a[j + 1] = temp;
k = 1;
}
}
if (k == 0)
break;
}
}
总结:
O(N^2)
最好情况是:O(N)
O(1)
快排的思路:
任取待排序元素序列中的某元素作为基准值,按照该基准值将排序分成两个子序列,左子序列的元素均小于基准值,右子序列的元素均大于基准值,,然后在左、右子序列重复上述过程,直到所有元素都有序为止
下面介绍快排的三个版本:
思路:
看完这个思路之后,我们直到整体思路就是,从左边找比基准值大的与右边找到的比基准值小的进行交换,但是在这里要思考清楚一个问题:
既然最后一步是将相遇的值与key的值交换,也就是说明左右相遇时指向的值一定比key小,这是为什么?
我们从两个情况去考虑:
我们以第一次循环为例:
这里我们就实现了把数组分成了两个子序列(左子序列的元素均小于基准值,右子序列的元素均大于基准值),然后我们把这两个子序列看成一个新的序列,对其指向执行相同的程序,并一直递归下去,直到 传进去的数组的 大小为 0 (没有左子序列 或 右子序列 也就是key比该序列的所有元素都要大或小) 或 1(没有执行程序的必要)。实际上执行的思路入下图所示
代码
void QuickSort1(int* a, int n) //hoare版本
{
if (n == 1 || n == 0)
return;
int key = 0;
int left = 0; //这里必须从0开始,如果从1开始,当数组只有两个的时候代码会出现问题
int right = n - 1;
while (left < right) //这里必须是小于号保证出循环left一定等于right
{
while (left<right && a[right] > a[key])
right--;
while (left<right && a[left] <= a[key]) //注意这里的大于等于号 因为key在左边,为了让key往左走必须大于等于
left++;
swap(&a[left], &a[right]);
}
swap(&a[left], &a[key]);
key = left;
QuickSort1(a, key);
QuickSort1(a + key + 1, n - key - 1);
}
思路:
我们以一次循环作为基础:
void QuickSort2(int* a, int n) //快排——挖坑法
{
if (n == 1 || n == 0)
return;
int key = 0;
int left = 0;
int right = n - 1;
int temp = a[key];
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; //最后左右相遇时,填坑
key = left;
QuickSort2(a, key);
QuickSort2(a + key + 1, n - key - 1);
}
基本思路: 设置两个指针(prev,cur),还是在数组的头或尾找一个基准值,prev指向第一个元素,cur指向第二个元素,然后cur开始遍历数组,找到比基准值小的值就与prev下一个元素交换,直到cur遍历完数组。
我们以一次循环作为基础:
如果没有遇到比基准值小的数cur就直接往后走,找到比基准值小的数就开始交换。
其实我们还可以把数组的最后一个数设为基准值,这时程序也要相应的做出一点改变
这时prev的其实位置就必须时数组第一个数的前一个位置,也就是下表为-1的位置,cur起始位置时数组的 首元素,其他的步骤与上面的一样。
代码
void QuickSort3(int* a, int n) //快排——双指针法
{
if (n == 1 || n == 0)
return;
int key = 0;
int prev = 0;
int cur = 1;
while (cur < n)
{
if (a[cur] < a[key])
swap(&a[++prev], &a[cur]);
cur++;
}
swap(&a[prev], &a[key]);
key = prev;
QuickSort3(a, key);
QuickSort3(a + key + 1, n - key - 1);
}
把递归转换成非递归有两种方法:
因为递归是把大事化成若干个小问题去解决,循环正好是反过来人为的从小问题开始解决。但是有时候递归在把大问题化成小问题的时候,小问题是建立在大问题的基础上的,不解决大问题就无法得到小问题,所以用循环就无法从小问题开始解决,这时就要用到栈的结构,栈的结构正好将这种情况完美解决,从 入栈时的大->小 ,到 出栈进入循环时的 小->大
本体思路:
注意:
这里要注意入栈和出栈时的左右边界的顺序,左边界先入栈,右边界后入栈,取栈顶元素时就要先取右边界并出栈,再取左边界
int Partsort(int* a, int left,int right) //单趟排序
{
int key = left;
int temp =Choose_Key(a,left,right);
swap(&a[temp], &a[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[left], &a[key]);
return left;
}
void QuickSortNonR(int* a, int n) //快排的非递归写法
{
Stack ps;
StackInit(&ps);
StackPush(&ps, 0); // 入左边界
StackPush(&ps, n - 1); // 入右边界
while (!StackEmpty(&ps))
{
int end = StackTop(&ps); //右边界先出栈 一定要和上面入栈的顺序相反!!!!
StackPop(&ps);
int begin = StackTop(&ps); //左边界出栈
StackPop(&ps);
int keyi = Partsort(a, begin, end);
if (keyi + 1 < end) //判断是否要继续递归
{
StackPush(&ps, keyi + 1); // 右子序列的 左边界入栈
StackPush(&ps, end); // 右子序列的 右边界入栈
}
if (begin < keyi-1)
{
StackPush(&ps, begin); //右子序列的 左边界入栈
StackPush(&ps, keyi - 1); // 右子序列的 右边界入栈
}
}
StackDestroy(&ps);
}
快排的时间复杂度最快的时候可以达到O(N*logN)
,但是当数组为有序的时候 或 有大面积重复的时候,时间复杂度会飙升到O(N^2)
为了解决这种情况,首先要了解为什么会出现这种情况?
最主要的原因是 key每次都取到了整个数组的 最小值或最大值 ,使二叉树变成单边树,这时我们只需要对key选取的值稍加处理就可以了,我们采用三数取中 即选取数组 左 、 中 、 右 大小在中间的数,与key所在的位置交换。
代码
int Choose_Key(int *a,int left, int right)
{
int mid = (left + right) / 2;
if (a[left] > a[right])
{
if (a[mid] > a[left])
return left;
else
{
if (a[right] > a[mid])
return right;
else
return mid;
}
}
else
{
if (a[mid] > a[right])
return right;
else
{
if (a[left] > a[mid])
return left;
else
return mid;
}
}
}
void QuickSort2(int* a, int n) //以快排——挖坑法为例
{
if (n == 1 || n == 0)
return;
int key = 0;
int t = Choose_Key(a, 0, n - 1); // 选出中间元素的下标
swap(&a[key], &a[t]); //与key所在位置的元素交换
int left = 0;
int right = n - 1;
int temp = a[key];
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;
key = left;
QuickSort2(a, key);
QuickSort2(a + key + 1, n - key - 1);
}
总结:
O(N*logN)
最坏的情况:O(N^2)
O(logN)
基本思想
归并排序(MERGE_SORT) 是建立在归并操作上的一种有效的排序算法,该算法采用分治法的一个典型用例。将已有序的子序列合并,得到完全有序的序列;即使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为二路归并。
如上图所示,单趟排序的过程是一个很经典的问题,创建一个额外的数组,将两个顺序的子序列排序,我们只要定义两个个指针,分别指向 待排序的两个序列,对指针所指的内容进行比较,如果满足条件就进入新数组,重复上述步骤直至所有元素都拷贝到新数组。
单趟排序思路了解之后,我们直到单趟排序需要两个有序的序列,所以就用递归将数组不断划分,当序列只有一个元素的时候我们认为他就是大小为1的有序序列,进行单趟排序。然后把排完序的子序列在进行合并的过程。
void _MergeSort(int* a, int left,int right,int *temp)
{
int mid = (left + right) / 2;
if (left >= right)
return;
_MergeSort(a, left, mid,temp);
_MergeSort(a, mid + 1, right,temp);
int cur1 = left, end1 = mid;
int cur2 = mid + 1, end2 = right;
int cur3 = left;
while (cur1 <= end1 && cur2 <= end2)
{
if (a[cur1] <= a[cur2])
{
temp[cur3] = a[cur1];
cur3++;
cur1++;
}
else
{
temp[cur3] = a[cur2];
cur3++;
cur2++;
}
}
while (cur1 <= end1)
{
temp[cur3] = a[cur1];
cur3++;
cur1++;
}
while (cur2 <= end2)
{
temp[cur3] = a[cur2];
cur3++;
cur2++;
}
for (int i = 0; i <= right; i++)
{
a[i] = temp[i];
}
}
void MergeSort(int* a, int n)
{
int* temp = (int*)malloc(n * sizeof(int));
_MergeSort(a, 0, n - 1, temp);
}
非递归的思路是
void fun(int cur1,int end1,int cur2,int end2,int cur3,int *temp,int *a)
{
while (cur1 <= end1 && cur2 <= end2)
{
if (a[cur1] <= a[cur2])
{
temp[cur3] = a[cur1];
cur3++;
cur1++;
}
else
{
temp[cur3] = a[cur2];
cur3++;
cur2++;
}
}
while (cur1 <= end1)
{
temp[cur3] = a[cur1];
cur3++;
cur1++;
}
while (cur2 <= end2)
{
temp[cur3] = a[cur2];
cur3++;
cur2++;
}
for (int i = 0; i <= end2; i++)
{
a[i] = temp[i];
}
}
void MergeSortNonR(int* a, int n)
{
int* p = (int*)malloc(n * sizeof(int));
int index = 2;
while ((n*2) / index)
{
int times = n / index;
if (n % index != 0)
times++;
for (int i = 0; i < times; i++)
{
int cur1 = i * index, end1 = cur1+ index/2 -1;
if (end1 > n - 1)
end1 = n - 1;
int cur2 = cur1 + index / 2;
int end2 = cur2 + index/2 - 1;
if (end2 > n - 1)
end2 = n - 1;
int cur3 = cur1;
fun(cur1, end1, cur2, end2, cur3, p, a);
}
index *= 2;
}
free(p);
}
总结:
O(N*logN)
O(N)
思想:
void CountSort(int* a, int n)
{
int max = a[0];
int min = a[0];
for (int i = 0; i < n; i++) //找出最大最小值确定开辟数组的范围
{
if (a[i] < min)
min = a[i];
if (a[i] > max)
max = a[i];
}
int range = max - min + 1;
int* p = (int*)calloc(range, sizeof(int));
for (int i = 0; i < n; i++)
{
p[a[i] - min]++;
}
int j = 0;
for (int i = 0; i < range; i++)
{
while (p[i])
{
a[j++] = i + min;
p[i]--;
}
}
}
总结:
O(max(N,最大值与最小值之间元素的个数))
O(范围)
排序方法 | 平均情况 | 最好情况 | 最坏情况 | 辅助空间 | 稳定性 |
---|---|---|---|---|---|
冒泡排序 | O(N^2) |
O(N) |
O(N^2) |
O(1) |
稳定 |
简单选择排序 | O(N^2) |
O(N^2) |
O(N^2) |
O(1) |
不稳定 |
直接插入排序 | O(N^2) |
O(N) |
O(N^2) |
O(1) |
稳定 |
希尔排序 | 大概O(N^1.2) |
不确定 | O(N^2) |
O(1) |
不稳定 |
堆排序 | O(N*logN) |
O(N*logN) |
O(N*logN) |
O(1) |
不稳定 |
归并排序 | O(N*logN) |
O(N*logN) |
O(N*logN) |
O(n) |
稳定 |
快速排序 | O(N*logN) |
O(N*logN) |
O(N^2) |
O(1) |
不稳定 |
计数排序 | O(max(N,最大值与最小值之间元素的个数)) |
- | - | O(范围) |
稳定 |