排序概念
排序
所谓排序,就是使一串记录,按照其中的某个或某些关键字的大小,递增或递减的排列起来的操作。
稳定性
假定在待排序的记录序列中,存在多个具有相同的关键字的记录,若经过排序,这些记录的相对次
序保持不变,即在原序列中,r[i]=r[j],且r[i]在r[j]之前,而在排序后的序列中,r[i]仍在r[j]之前,则称这种排序算法是稳定的;否则称为不稳定的。
内部排序
数据元素全部放在内存中的排序。
外部排序
数据元素太多不能同时放在内存中,根据排序过程的要求不能在内外存之间移动数据的排序。
排序算法按数据交互方式分,可分为以下几种
1、插入排序:将数据插入到有序序列中的特定位置
2、选择排序:选择符合要求的数据放入相应位置
3、交换排序:通过交换数据将它们变得有序
4、归并排序:归并两个有序数组
插入排序
把待排序的记录按其关键码值的大小逐个插入到一个已经排好序的有序序列中,直到所有的记录插入完为止,得到一个新的有序序列 。
实际中我们玩扑克牌时,就用了插入排序的思想
直接插入排序
当插入第i(i>=1)个元素时,前面的array[0],array[1],…,array[i-1]已经排好序,此时用array[i]的排序码与
array[i-1],array[i-2],…的排序码顺序进行比较,找到插入位置即将array[i]插入,原来位置上的元素顺序后移
画图解析
这便完成单趟排序
int end = i;//有序序列的末尾
int x = a[end + 1];//待排数
while (end >= 0)
{
if (a[end] > x)
{
a[end + 1] = a[end];//后移
--end;
}
else
{
break;
}
}
a[end + 1] = x;//插入
我们可以从前往后遍历待排序数组,将已经遍历的数组看做是有序数组,而后面的数据是我们待排序的数据
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)
{
if (a[end] > x)
{
a[end + 1] = a[end];//后移
--end;
}
else
{
break;
}
}
a[end + 1] = x;//插入
}
}
插入排序特性
- 元素集合越接近有序,直接插入排序算法的时间效率越高
- 时间复杂度:O(N^2)
- 空间复杂度:O(1),它是一种稳定的排序算法
- 稳定性:稳定
而我们可以利用在有序或者接近有序的情况下,插入排序效率较高这一特征,对插入排序进行优化,而这个优化方式就叫希尔排序
希尔排序
希尔排序法又称缩小增量法。希尔排序法的基本思想是:先选定一个整数,把待排序文件中所有记录分成个组,所有距离为的记录分在同一组内,并对每一组内的记录进行排序。然后,取,重复上述分组和排序的工作。当到达=1时,所有记录在统一组内排好序。
单组预排序
int gap = 3;
int end = i;//与插入排序类似
int x = a[end + gap];
while (end >= 0)//隔着gap排
{
if (a[end] > x)
{
a[end + gap] = a[end];
end -= gap;
}
else
{
break;
}
}
a[end + gap] = x;//插入
多组排
int gap = 3;
for (int i = 0; i < n - gap; i += gap) //多组
{
int end = i;//与插入排序类似
int x = a[end + gap];
while (end >= 0)//隔着gap,分组排
{
if (a[end] > x)
{
a[end + gap] = a[end];
end -= gap;
}
else
{
break;
}
}
a[end + gap] = x;//插入
}
}
红、蓝、紫组一起排
int gap = 3;
for (int j = 0; j < gap; j++) //一起排
{
for (int i = j; i < n - gap; i += gap) //0换成j
{
int end = i;//与插入排序类似
int x = a[end + gap];
while (end >= 0)//隔着gap,分组排
{
if (a[end] > x)
{
a[end + gap] = a[end];
end -= gap;
}
else
{
break;
}
}
a[end + gap] = x;//插入
}
}
稍微进行修改,可以进行“一锅炖”,多组并排
int gap = 3;
for (int i = 0; i < n - gap; ++i) //多组并排,++i
{
int end = i;//与插入排序类似
int x = a[end + gap];
while (end >= 0)//隔着gap,分组排
{
if (a[end] > x)
{
a[end + gap] = a[end];
end -= gap;
}
else
{
break;
}
}
a[end + gap] = x;//插入
}
当gap=1的时候,便是直接插入排序,总体思想便是多次预排序(gap>1)和单次直接插入排序(gap==1)
void shellsort(int* a, int n)
{
assert(a);
int gap = n;
while(gap>1)
{
gap = gap / 3 + 1;//经测试,gap/3是最优,+1是为了不让gap为0
for (int i = 0; i < n - gap; i+=gap)
{
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;
}
}
}
选择排序
每一次从待排序的数据元素中选出最小(或最大)的一个元素,存放在序列的起始位置,直到全部待排序的数据元素排完
直接选择排序
遍历一遍,选出最大(小)数,若它不是这组元素中的最后一个(第一个)元素,则将它与这组元素中的最后一个(第一个)元素交换,然后在第二到倒数第二的集合中重复上述步骤
单趟排序:
int begin = 0, end = n - 1;
int mini = begin, maxi = begin;
for (int i = begin; i <= end; ++i)
{
if (a[i] < a[mini])//找小
{
mini = i;
}
if (a[i] > a[maxi])//找大
{
maxi = i;
}
}
Swap(&a[begin], &a[mini]);//交换小
if (begin == maxi)//如果最大在开头,交换会有BUG
{
maxi = mini;
}
Swap(&a[end], &a[maxi]);//交换大
总
void SelectSort(int* a, int n)
{
int begin = 0, end = n - 1;
while (begin < end)
{
int mini = begin, maxi = begin;
for (int i = begin; i <= end; ++i)
{
if (a[i] < a[mini])//找小
{
mini = i;
}
if (a[i] > a[maxi])//找大
{
maxi = i;
}
}
Swap(&a[begin], &a[mini]);//交换小
if (begin == maxi)//如果最大在开头,交换会有BUG
{
maxi = mini;
}
Swap(&a[end], &a[maxi]);//交换大
++begin;
--end;//第二到倒数第二的集合中重复上述步骤
}
}
直接选择排序特性
- 直接选择排序思考非常好理解,但是效率不是很好。实际中很少使用
- 时间复杂度:O(N^2)
- 空间复杂度:O(1)
- 稳定性:不稳定
堆排序
堆排序是指利用堆积树(堆)这种数据结构所设计的一种排序算法,它是选择排序的一种。它是通过堆来进行选择数据。需要注意的是排升序要建大堆,排降序建小堆。
堆排序代码较复杂,这里不作演示
堆排序特性
- 堆排序使用堆来选数,效率就高了很多。
- 时间复杂度:O(N*logN)
- 空间复杂度:O(1)
- 稳定性:不稳定
交换排序
所谓交换,就是根据序列中两个记录键值的比较结果来对换这两个记录在序列中的位置,交换排
序的特点是:将键值较大的记录向序列的尾部移动,键值较小的记录向序列的前部移动。
冒泡排序
每次通过两两相邻的元素交换,将最大的元素交换到数组的末尾,就像冒泡一样把最大元素冒在上面
void BubbleSort(int* a, int n)
{
int end = n;
while (end > 0)
{
int exchange = 0;
for (int i = 1; i < end; ++i)
{
if (a[i - 1] > a[i])
{
exchange = 1;
Swap(&a[i - 1], &a[i]);
}
}
--end;
if (exchange == 0)//说明已经有序,不用再排
{
break;
}
}
}
冒泡排序特性
- 冒泡排序是一种非常容易理解的排序
- 时间复杂度:O(N^2)
- 空间复杂度:O(1)
- 稳定性:稳定
快速排序
快速排序是Hoare于1962年提出的一种二叉树结构的交换排序方法,其基本思想为:任取待排序元素序列中的某元素作为基准值,按照该排序码将待排序集合分割成两子序列,左子序列中所有元素均小于基准值,右子序列中所有元素均大于基准值,然后最左右子序列重复该过程,直到所有元素都排列在相应位置上为止。
hoare版本
单趟排序的目标:左边的值比基准小,右边的值比基准大
int PartSort1(int* a, int left, int right)
{
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]);
return left;
}
如果用最左边数做Key,右边先走,反之,最右边数做Key,左边先走。
当我们的key选择不好时,会另快排的效率下降,我们可以对选key进行优化,如三数取中。
int PartSort1(int* a, int left, int right)
{
// 三数取中 -- 面对有序最坏情况,变成选中位数做key,变成最好情况
int mini = GetMidindex(a, left, right);
Swap(&a[mini], &a[left]);
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]);
return left;
}
挖坑法
hoare版本的变化,单趟排序目标与hoare版本一样
int PartSort2(int* a, int left, int right)
{
int mini = GetMidindex(a, left, right);
Swap(&a[mini], &a[left]);
int key = a[left];
int pivot = left;//坑
while (left < right)
{
while (left < right && a[right] >= key)//R先走
{
--right;
}
a[pivot] = a[right];
pivot = right;//交换
while (left < right && a[left] <= key)//L后走
{
++left;
}
a[pivot] = a[left];
pivot = left;//交换
}
a[pivot] = key;//把key放到坑
return pivot;
}
前后指针
int PartSort3(int* a, int left, int right)
{
int mini = GetMidindex(a, left, right);
Swap(&a[mini], &a[left]);
int keyi = left;
int prev = left;
int cur = prev + 1;
while (cur <= right)
{
if (a[cur] < a[keyi] && ++prev != cur)
{
Swap(&a[cur], &a[prev]);
}
++cur;
}
Swap(&a[prev], &a[keyi]);
return prev;
}
总体排序
如何进行总体排序呢?当我们排完一次序,相当于分割成2部分,可用二叉树的思想,利用递归进行总体排序。
void QuickSort(int* a, int left, int right)
{
if (left >= right)
return;
int keyi = PartSort1(a, left, right);//可以换成上面3种排序方法
// [left, keyi-1] keyi [keyi+1, right]
QuickSort(a, left, keyi - 1);
QuickSort(a, keyi + 1, right);
}
也可用非递归的方法,但需要用上栈
void QuickSortNonR(int* a, int left, int right)
{
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 keyi = PartSort3(a, begin, end);
if (keyi + 1 < end)
{
StackPush(&st, keyi + 1);
StackPush(&st, end);
}
if (begin < keyi - 1)
{
StackPush(&st, begin);
StackPush(&st, keyi - 1);
}
}
StackDestory(&st);
}
快排特性
归并排序
归并排序(MERGE-SORT)是建立在归并操作上的一种有效的排序算法,该算法是采用分治法(Divide andConquer)的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为二路归并。
void _MergeSort(int* a, int left, int right, int* tmp)
{
if (left >= right)
{
return;
}
int mid = (left + right) / 2;
// [left, mid] [mid+1, right] 有序
_MergeSort(a, left, mid, tmp);
_MergeSort(a, mid + 1, right, tmp);
int begin1 = left, end1 = mid;
int begin2 = mid+1, end2 = right;
int i = left;
while (begin1 <= end1 && begin2 <= end2)
{
if (a[begin1] < a[begin2])
{
tmp[i++] = a[begin1++];
}
else
{
tmp[i++] = a[begin2++];
}
}
while (begin1 <= end1)
{
tmp[i++] = a[begin1++];
}
while (begin2 <= end2)
{
tmp[i++] = a[begin2++];
}
// tmp 数组拷贝回a
for (int j = left; j <= right; ++j)
{
a[j] = tmp[j];
}
}
void MergeSort(int* a, int n)
{
int* tmp = (int*)malloc(sizeof(int) * n);
if (tmp == NULL)
{
printf("malloc fail\n");
exit(-1);
}
_MergeSort(a, 0, n - 1, tmp);
free(tmp);
tmp = NULL;
}
归并排序特性
- 归并的缺点在于需要O(N)的空间复杂度,归并排序的思考更多的是解决在磁盘中的外排序问题。
- 时间复杂度:O(N*logN)
- 空间复杂度:O(N)
- 稳定性:稳定