1.排序的概念及其运用
1.1排序的概念
排序:所谓排序,就是使一串记录,按照其中的某个或某些关键字的大小,递增或递减的排列起来的操作。
稳定性:假定在待排序的记录序列中,存在多个具有相同的关键字的记录,若经过排序,这些记录的相对次 序保持不变,即在原序列中,r[i]=r[j],且r[i]在r[j]之前,而在排序后的序列中,r[i]仍在r[j]之前,则称这种排 序算法是稳定的;否则称为不稳定的。
内部排序:数据元素全部放在内存中的排序。
外部排序:数据元素太多不能同时放在内存中,根据排序过程的要求不能在内外存之间移动数据的排序。
1.2排序运用


1.3 常见的排序算法

2、常见排序算法思想
2.1 插入排序
2.11 基本思想:
把待排序的记录按其关键码值的大小逐个插入到一个已经排好序的有序序列中,直到所有的 记录插入完为止,得到一个新的有序序列 。当我们排序扑克牌是就是用的这种思想。

2.12 直接插入排序
当插入第i(i>=1)个元素时,前面的array[0],array[1],...,array[i-1]已经排好序,此时用array[i]的排序码与 array[i-1],array[i-2],...的排序码顺序进行比较,找到插入位置即将array[i]插入,原来位置上的元素顺序后移。

代码演示:
cpp
void InsertSort(int* a, int n)
{
for (int i = 0;i < n - 1;i++)
{
int tmp = a[i + 1];
int end = i;
while (end >= 0)
{
if (a[end] > tmp)
{
a[end + 1] = a[end];
end--;
}
else
{
break;
}
}
a[end + 1] = tmp;
}
}
注:插入排序一般都是在原数组上进行操作,不用再创建一个数组 。可以结合代码和上图一同看。
直接插入排序的特性总结:
-
元素集合越接近有序,直接插入排序算法的时间效率越高
-
时间复杂度:O(N^2)
-
空间复杂度:O(1),它是一种稳定的排序算法
-
稳定性:稳定
2.1.3 希尔排序( 缩小增量排序 )
希尔排序法又称缩小增量法。希尔排序法的基本思想是:先选定一个整数,把待排序文件中所有记录分成个 组,所有距离为的记录分在同一组内,并对每一组内的记录进行排序。然后,取,重复上述分组和排序的工 作。当到达=1时,所有记录在统一组内排好序。

简单来说希尔排序就是把一个数组变得比较有序,然后再进行插入排序。因为通过简单的计算我们能发现,插入排序在有序的情况下时间复杂度是O(N)。
代码演示:
cpp
void ShellSort(int* a, int n)
{
int gap = n;
while (gap > 1)
{
gap /= 2;
for (int i = 0;i < n - gap;i++)
{
int tmp = a[i + gap];
int end = i;
while (end >= 0)
{
if (a[end] > tmp)
{
a[end + gap] = a[end];
end = end - gap;
}
else
{
break;
}
}
a[end + gap] = tmp;
}
}
}
注:如果是一趟一趟看的话建议可以多加一层循环,时间复杂度还是不变的。这里gap就相当于前面插入排序的1,只不过这里跳的步数大。
-
希尔排序是对直接插入排序的优化。
-
当gap > 1时都是预排序,目的是让数组更接近于有序。当gap == 1时,数组已经接近有序的了,这样就 会很快。这样整体而言,可以达到优化的效果。我们实现后可以进行性能测试的对比。
-
希尔排序的时间复杂度不好计算,因为gap的取值方法很多,导致很难去计算,因此在好些树中给出的 希尔排序的时间复杂度都不固定。
-
稳定性:不稳定
2.2 选择排序
2.2.1基本思想:
每一次从待排序的数据元素中选出最小(或最大)的一个元素,存放在序列的起始位置,直到全部待排序的 数据元素排完 。
2.2.2 直接选择排序:
在元素集合array[i]--array[n-1]中选择关键码最大(小)的数据元素 若它不是这组元素中的最后一个(第一个)元素,则将它与这组元素中的最后一个(第一个)元素交换 在剩余的array[i]--array[n-2](array[i+1]--array[n-1])集合中,重复上述步骤,直到集合剩余1个元素

代码演示:
cpp
void swap(int* p1, int* p2)
{
int tmp = *p1;
*p1 = *p2;
*p2 = tmp;
}
void SelectSort(int* a, int n)
{
int left = 0;
int right = n - 1;
int max = 0;
int min = 0;
while (left < right)
{
max = left;
min = right;
for (int i = left;i <= right;i++)
{
if (a[i] > a[max])
{
max = i;
}
if (a[i] < a[min])
{
min = i;
}
}
swap(&a[left], &a[min]);
if (left == max)
{
max = min;
}
swap(&a[right], &a[max]);
left++;
right--;
}
}
注:代码中的swap函数为交换函数 ,当左指针指向的值为max时,我们需要加一个把max的值重新换回来的判断,否则会将最大值与最小值交换,那么最大值的指针指向的就不是最大值了。反之同理。
直接选择排序的特性总结:
-
直接选择排序思考非常好理解,但是效率不是很好。实际中很少使用
-
时间复杂度:O(N^2)
-
空间复杂度:O(1)
-
稳定性:不稳定
2.2.3 堆排序
堆排序(Heapsort)是指利用堆积树(堆)这种数据结构所设计的一种排序算法,它是选择排序的一种。它是 通过堆来进行选择数据。需要注意的是排升序要建大堆,排降序建小堆。

代码演示:
cpp
void swap(int* p1, int* p2)
{
int tmp = *p1;
*p1 = *p2;
*p2 = tmp;
}
void adjustdwon(int* a, int n, int parent)
{
int child = (parent * 2) + 1;
while (child < n)
{
if (child + 1 < n && a[child] < a[child + 1])
{
child++;
}
if (a[child] > a[parent])
{
swap(&a[child], &a[parent]);
parent = child;
child = (parent * 2) + 1;
}
else
{
break;
}
}
}
void HeapSort(int* a, int n)
{
int i = 0;
for (i = (n - 1 - 1) / 2;i >= 0;i--)
{
adjustdwon(a, n, i);
}
int end = n - 1;
while (end > 0)
{
swap(&a[end], &a[0]);
adjustdwon(a, end, 0);
end--;
}
}
注: 因为向下调整建大堆时,要左右子树都为大堆因此我们要从最后一个不是叶子节点的 位置开始向下调整建大堆,而这个位置就是最后一个叶子节点的父亲。每次拿到最大值以后与最后位置交换,之后就不要动最大值了。
直接选择排序的特性总结:
-
堆排序使用堆来选数,效率就高了很多。
-
时间复杂度:O(N*logN)
-
空间复杂度:O(1)
-
稳定性:不稳定
2.3 交换排序
基本思想:所谓交换,就是根据序列中两个记录键值的比较结果来对换这两个记录在序列中的位置,交换排 序的特点是:将键值较大的记录向序列的尾部移动,键值较小的记录向序列的前部移动。
2.3.1冒泡排序
基本思想:两个数字相互比较大的换到小的前面,如此反复。当一趟走完以后,最大的会在最后面。

代码演示:
cpp
void swap(int* p1, int* p2)
{
int tmp = *p1;
*p1 = *p2;
*p2 = tmp;
}
void BubbleSort(int* a, int n)
{
for (int j = 0;j < n;j++)
{
for (int i = 1;i < n - j;i++)
{
if (a[i - 1] > a[i])
{
swap(&a[i - 1], &a[i]);
}
}
}
}
冒泡排序的特性总结:
-
冒泡排序是一种非常容易理解的排序
-
时间复杂度:O(N^2)
-
空间复杂度:O(1)
-
稳定性:稳定
2.3.2 快速排序
快速排序是Hoare于1962年提出的一种二叉树结构的交换排序方法,其基本思想为:任取待排序元素序列中 的某元素作为基准值,按照该排序码将待排序集合分割成两子序列,左子序列中所有元素均小于基准值,右 子序列中所有元素均大于基准值,然后最左右子序列重复该过程,直到所有元素都排列在相应位置上为止。
- hoare版本
可以先看一个动图:


代码演示:
cpp
void swap(int* p1, int* p2)
{
int tmp = *p1;
*p1 = *p2;
*p2 = tmp;
}
int getmidi(int* a, int left, int right)
{
int midi = (left + right) / 2;
if (a[left] < a[right])
{
if (a[midi] > a[left])
{
return midi;
}
else if (a[midi] < a[left])
{
return left;
}
else
{
return right;
}
}
//(a[left]>a[right])
else
{
if (a[midi] >a[right])
{
return midi;
}
else if (a[midi] < a[right])
{
return right;
}
else
{
return left;
}
}
}
int PartSort1(int* a, int left, int right)
{
//随机取值
/*int randi = left + (rand() % (right - left));
swap(&a[left], &a[randi]);
int keyi = left;*/
//三数取中
int midi = getmidi(a, left, right);
swap(&a[left], &a[midi]);
int keyi = left;
while (left < right)
{
while (left < right && a[right]>=a[keyi])
{
right--;
}
while (left < right && a[left] <= a[keyi])
{
left++;
}
swap(&a[left], &a[right]);
}
swap(&a[left], &a[keyi]);
keyi = left;
return keyi;
}
void QuickSort(int* a, int left, int right)
{
if (left >= right)
{
return;
}
int keyi = PartSort1(a, left, right);
QuickSort(a, left, keyi - 1);
QuickSort(a, keyi + 1, right);
}
注:代码里面的随机取值是为了让key值取一个比较随机的值,因为当数组为有序时,如果用常规的Hoare快排会让时间复杂度变成(N^2)下面将用一个图解释一下。

这里一般是有两种方法第一个就是取随机值像上面代码呈现的一样,第二个就是三数取中在左边中间右边位置,取一个中间值。如果遇到大量重复数据,快排的效率会大大降低,这里就需要三路划分的方法。( keyi代表key值的下标)
- 挖坑法

挖坑其实和 hoare版本十分相似,唯一不同的就是将key值拿到外面去制造一个坑位(这里注意还是要保持左边做key右边先走的思想,这里是左边的值拿出来右边就先走)然后右边找到一个小于key值的数放到坑里。左边再找小于key的值放到右边坑位,如此反复。
代码演示:
cpp
void swap(int* p1, int* p2)
{
int tmp = *p1;
*p1 = *p2;
*p2 = tmp;
}
int getmidi(int* a, int left, int right)
{
int midi = (left + right) / 2;
if (a[left] < a[right])
{
if (a[midi] > a[left])
{
return midi;
}
else if (a[midi] < a[left])
{
return left;
}
else
{
return right;
}
}
//(a[left]>a[right])
else
{
if (a[midi] >a[right])
{
return midi;
}
else if (a[midi] < a[right])
{
return right;
}
else
{
return left;
}
}
}
int PartSort2(int* a, int left, int right)
{
int midi = getmidi(a, left, right);
if (midi != left)
swap(&a[left], &a[midi]);
int key = a[left];
int hole = left;
while (left < right)
{
while (left < right && a[right] >= key)
right--;
a[hole] = a[right];
hole = right;
while (left < right && a[left] <= key)
left++;
a[hole] = a[left];
hole = left;
swap(&a[left], &a[hole]);
}
a[hole] = key;
return hole;
}
void QuickSort(int* a, int left, int right)
{
if (left >= right)
{
return;
}
int keyi = PartSort2(a, left, right);
QuickSort(a, left, keyi - 1);
QuickSort(a, keyi + 1, right);
}
- 前后指针版本


cpp
void swap(int* p1, int* p2)
{
int tmp = *p1;
*p1 = *p2;
*p2 = tmp;
}
int getmidi(int* a, int left, int right)
{
int midi = (left + right) / 2;
if (a[left] < a[right])
{
if (a[midi] > a[left])
{
return midi;
}
else if (a[midi] < a[left])
{
return left;
}
else
{
return right;
}
}
//(a[left]>a[right])
else
{
if (a[midi] >a[right])
{
return midi;
}
else if (a[midi] < a[right])
{
return right;
}
else
{
return left;
}
}
}
int PartSort3(int* a, int left, int right)
{
int midi = getmidi(a, left, right);
if (midi != left)
swap(&a[left], &a[midi]);
int prev = left;
int cur = left + 1;
int keyi = left;
while (cur <= right)
{
if (a[cur] < a[keyi])
{
prev++;
swap(&a[cur], &a[prev]);
cur++;
}
else
{
cur++;
}
}
swap(&a[keyi], &a[prev]);
keyi = prev;
return keyi;
}
void QuickSort(int* a, int left, int right)
{
if (left >= right)
{
return;
}
int keyi = PartSort3(a, left, right);
QuickSort(a, left, keyi - 1);
QuickSort(a, keyi + 1, right);
}
2.3.3 快速排序非递归
因为快排与二叉树的前序遍历十分相像,因此快排的非递归我们能用栈来实现。
具体步骤如下图:

cpp
void swap(int* p1, int* p2)
{
int tmp = *p1;
*p1 = *p2;
*p2 = tmp;
}
int getmidi(int* a, int left, int right)
{
int midi = (left + right) / 2;
if (a[left] < a[right])
{
if (a[midi] > a[left])
{
return midi;
}
else if (a[midi] < a[left])
{
return left;
}
else
{
return right;
}
}
//(a[left]>a[right])
else
{
if (a[midi] >a[right])
{
return midi;
}
else if (a[midi] < a[right])
{
return right;
}
else
{
return left;
}
}
}
void QuickSortNonR(int* a, int left, int right)
{
st st;
stinit(&st);
stpush(&st, right);
stpush(&st, left);
while (!stempty(&st))
{
int begin = sttop(&st);
stpop(&st);
int end = sttop(&st);
stpop(&st);
int keyi= PartSort3(a, begin,end);
if (keyi + 1 < end)
{
stpush(&st, end);
stpush(&st, keyi + 1);
}
if ( keyi - 1>begin)
{
stpush(&st, keyi-1);
stpush(&st, left);
}
}
}
快速排序的特性总结:
-
快速排序整体的综合性能和使用场景都是比较好的,所以才敢叫快速排序
-
时间复杂度:O(N*logN)

-
空间复杂度:O(logN)
-
稳定性:不稳定
2.4 归并排序
基本思想:
归并排序(MERGE-SORT)是建立在归并操作上的一种有效的排序算法,该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有 序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为二路归并。
归并排序核心步骤:



cpp
void _MergeSort(int*a, int* tmp, int begin,int end )
{
if (begin >= end)
{
return;
}
int mid = (end + begin) / 2;
_MergeSort(a, tmp, begin,mid);
_MergeSort(a, tmp, mid+1, end);
int begin1 = begin, end1 = mid;
int begin2 = mid + 1, end2 = end;
int j = begin;
while (begin1<=end1 && begin2<=end2)
{
if (a[begin1] < a[begin2])
{
tmp[j++] = a[begin1++];
}
else
{
tmp[j++] = a[begin2++];
}
}
while (begin1 <= end1)
{
tmp[j++] = a[begin1++];
}
while (begin2 <= end2)
{
tmp[j++] = a[begin2++];
}
memcpy(a + begin, tmp + begin, sizeof(int)*(end - begin + 1));
}
void MergeSort(int* a, int n)
{
int* tmp = (int*)malloc(sizeof(int) * n);
if (tmp == NULL)
{
perror("malloc fail");
return;
}
_MergeSort(a, tmp, 0, n - 1);
free(tmp);
}
2.4.1 归并排序的非递归

cpp
void MergeSortNonR(int* a, int n)
{
int* tmp = (int*)malloc(sizeof(int) * n);
if (tmp == NULL)
{
perror("malloc fail");
return;
}
int gap = 1;
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;
if (end1 >= n || begin2 >= n)
{
break;
}
if (end2 >= n)
{
end2 = n - 1;
}
int j = i;
while (begin1 <= end1 && begin2 <= end2)
{
if (a[begin1] < a[begin2])
{
tmp[j++] = a[begin1++];
}
else
{
tmp[j++] = a[begin2++];
}
}
while (begin1 <= end1)
{
tmp[j++] = a[begin1++];
}
while (begin2 <= end2)
{
tmp[j++] = a[begin2++];
}
memcpy(a + i, tmp + i, sizeof(int) * (end2 - i + 1));
}
gap = 2 * gap;
}
}
注:begin1 end1 begin2 end 2那部分代码不理解的可以自己套数
归并排序的特性总结:
-
归并的缺点在于需要O(N)的空间复杂度,归并排序的思考更多的是解决在磁盘中的外排序问题。
-
时间复杂度:O(N*logN)
-
空间复杂度:O(N)
-
稳定性:稳定
2.5 非比较排序
思想:计数排序又称为鸽巢原理,是对哈希直接定址法的变形应用。
操作步骤: 1. 统计相同元素出现次数
- 根据统计的结果将序列回收到原来的序列中

cpp
void CountSort(int* a, int n)
{
int max = a[0];
int min = a[0];
for (int i = 1;i < n;i++)
{
if (max < a[i])
{
max = a[i];
}
if (min > a[i])
{
min = a[i];
}
}
int range = max - min+1;
int* counta = (int*)malloc(sizeof(int) * range);
if (counta == NULL)
{
perror("malloc fail");
return;
}
memset(counta, 0, sizeof(int) * range);
//计数
for (int i = 0;i < n;i++)
{
counta[a[i] - min]++;
}
//排序
int j = 0;
for (int i = 0;i< range;i++)
{
while (counta[i]--)
{
a[j++] = i+ min;
}
}
free(counta);
}
注:这里计数数组映射的相对值,因此最后取出来要加最小值。
计数排序的特性总结:
-
计数排序在数据范围集中时,效率很高,但是适用范围及场景有限。
-
时间复杂度:O(MAX(N,范围))
-
空间复杂度:O(范围) 比特科技
-
稳定性:稳定
3.排序算法复杂度及稳定性分析
