排序
1.排序的概念和应用
1.1.概念
排序:所谓排序,就是使一串记录,按照其中某个或某些关键字的大小,递增或递减的排列起来的操作
1.2.应用
购物筛选数据
如图所示在购买电脑时,可以根据销量,价格,品牌,好评率等等方式进行排序筛选合适的产品
1.3.常见的排序算法

常见的排序算法如图所示
2.实现常见排序算法
c
int a[] = {5, 3, 9, 6, 2, 4, 7, 1, 8};
2.1.插入排序
基本思想 :直接插入排序是一种简单的插入排序算法,其基本思想是:把待排序的记录按其关键码值的大小逐个插入到一个已经排序好的有序序列中,直到所有的记录插入完为止,得到一个新的有序序列。
实际上,在我们玩扑克牌时就会使用插入排序的方式把牌一张一张的插入到已经排序好的牌组中
2.1.1.直接插入排序
当插入第i(i>=1)个元素时,前面的a[0~i-1]已经排好序,此时用a[i]的排序码与a[i-1],a[i-2]...的排序码进行比较,找到插入位置即将a[i]插入,原来位置上的元素顺序后移
c
void InsertSort(int* a, int n)
{
for (int i = 0; i < n-1; i++)
{
int end = i ;
int tmp = a[end + 1];
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.2.希尔排序
希尔排序又称缩小增量法,希尔排序的基本思想是:先选定一个数整数(通常是gap = n/3+1),把待排序文件所有记录分成各组,所有的距离相等的记录分在同一组内,并对每一组内的记录进行排序,然后gap = gap/3+1得到下一个整数,再将数组分为各组,进行插入排序,当gap=1时,就相当于直接插入排序
它是在直接插入排序算法的基础上进行改进来的,综合来说它的效率肯定是高于直接排序算法的
希尔排序的特性
①:希尔排序是对直接插入排序的优化
②:当gap > 1时都是预排序,目的是让数组更接近有序,当gap == 1时,数组已经接近的了,这样就会很快,整体而言可以达到优化的效果
c
void ShellSort(int* a, int n)
{
int gap = n;
while (gap > 1)
{
//推荐写法:除3
gap = gap / 3 + 1;
for (int i = 0; i < n - gap; i++)
{
int end = i;
int tmp = a[end + gap];
while (end >= 0)
{
if (a[end] > tmp)
{
a[end + gap] = a[end];
end -= gap;
}
else
{
break;
}
}
a[end + gap] = tmp;
}
}
}
希尔排序的时间复杂度不好计算,因为gap的取值很多,导致很难去计算,因此很多书中给出的希尔排序的时间复杂度都不固定,《数据结构C语言版》----严蔚敏书中给出的时间复杂度为
2.2.选择排序
选择排序的基本思想:
每一次从待排序的数据元素中选出最小(或最大)的一个元素,存放在序列的起始位置,直到全部待排序的数据元素排完
2.2.1.直接选择排序
①:在元素集合array[i]--array[n-1] 中选择关键码最大(小)的数据元素
②:若它不是这组元素中的最后一个(第一个)元素,则将它与这组元素中的最后一个(第一个)元素交换
③:在剩余的array[i]--array[n-2](array[i+1]--array[n-1]) 集合中,重复上述步骤,直到集合剩余1个元素
c
void SelectSort(int* arr, int n)
{
int begin = 0, end = n - 1;
while (begin < end)
{
int mini = begin;
int maxi = begin;
for (int i = begin+1; i <= end; i++)
{
if (arr[i] < arr[mini])
{
mini = i;
}
if (arr[i] > arr[maxi])
{
maxi = i;
}
}
//maxi mini begin end
if (maxi == begin)
{
maxi = mini;
}
Swap(&arr[begin], &arr[mini]);
Swap(&arr[end], &arr[maxi]);
begin++;
end--;
}
}
直接选择排序的特性
①:直接选择排序的思想容易理解,但是效率不是很好,实际中很少使用
②:时间复杂度:O(N^2)
③:空间复杂度:O(1)
2.2.2.堆排序
堆排序是指利用堆积树这种数据结构设计的一种排序算法,他是选择排序的一种,它是通过堆来进行选择数据。需要注意的是排升序要用大堆,排降序要用小堆
c
//向下调整算法 logn
void AdjustDown(int* arr, int parent, int n)
{
int child = parent * 2 + 1;
while (child < n)
{
//建大堆:<
//建小堆: >
if (child + 1 < n && arr[child] < arr[child + 1])
{
child++;
}
//孩子和父亲比较
//建大堆:>
//建小堆:<
if (arr[child] > arr[parent])
{
Swap(&arr[child], &arr[parent]);
parent = child;
child = parent * 2 + 1;
}
else {
break;
}
}
}
//堆排序------------使用的是堆结构的思想 n * logn
void HeapSort(int* arr, int n)
{
//向下调整算法------建堆n
for (int i = (n - 1 - 1) / 2; i >= 0; i--)
{
AdjustDown(arr, i, n);
}
////向上调整算法建堆n*logn
//for (int i = 0; i < n; i++)
//{
// AdjustUp(arr, i);
//}
//n*logn
int end = n - 1;
while (end > 0)
{
Swap(&arr[0], &arr[end]);
AdjustDown(arr, 0, end);//logn
end--;
}
}
2.3.交换排序
交换排序的基本思想 :所谓交换,就是根据序列中两个记录键值的比较结果来对换这两个记录再序列中的位置
交换排序的特点:将键值较大的记录向序列尾部移动,键值较小的向序列前部移动
2.3.1.冒泡排序
冒泡排序是一种最基础的交换排序,之所以叫冒泡排序是因为每一个元素都可以像一个小气泡一样,根据自身大小一点一点向数组的一侧移动
c
void BubbleSort(int* a, int n)
{
int exchange = 0;
for (int i = 0; i < n; i++)
{
for (int j = 0; j <n-i-1 ; j++)
{
if (a[j] > a[j + 1])
{
exchange = 1;
swap(&a[j], &a[j + 1]);
}
}
if (exchange == 0)
{
break;
}
}
}
冒泡排序的特性 :
①:时间复杂度:O(N^2)
②:空间复杂度:O(1)
2.3.2.快速排序
快速排序是Hoare于1962年提出的一种二叉树结构的交换方法,其基本思想为:任取待排序元素序列中的某元素作为基准值,按照该排序码将待排序集合分割成两子序列,左子序列中的元素均小于基准值,右子序列中的元素均大于基准值,然后左右序列重复该过程,直到所有元素都排列到相应的位置上为止
快速排序实现主框架:
c
//快速排序
void QuickSort(int* a, int left, int right)
{
if (left <= right)
{
return;
}
//_QuickSort用于按照基准值将区间(left,right)中的元素进行划分
int meet = _QuickSort(a, left, right);
QuickSort(a, left, meet - 1);
QuickSort(a, meet + 1, right);
}
将区间中的元素进行划分的_QuickSort方法主要有下面几种实现方式:
2.3.2.1.hoare版本
算法思路 :
①:创建左右指针,确定基准值
②:从右向左找出比基准值小的数据,从左向右找出比基准值大的数据,左右指针数据交换,进入下次循环
问题 :为什么跳出循环后right 位置的值一定不大于key ?
答 :当left >right 时,即right 走到left 的左侧,而left 扫描过的数据均不大于key ,因此right 此时指向的数据不一定大于key
问题 :为什么left 和right 指定的数据和key 值相等时也要交换?
答 :相等的值参与交换确实有一些额外消耗,实际还有各种复杂的场景,假设数组中的数据大量重复时,无法进行有效的分割
c
//找基准值 hoare版本
int _QuickSort1(int* arr, int left, int right)
{
int keyi = left;
left++;
while (left <= right)
{
//right:从右往左找比基准值要小的
while (left <= right && arr[right] > arr[keyi])
{
--right;
}
//left:从左往右找比基准值要大的
while (left <= right && arr[left] < arr[keyi])
{
++left;
}
//left和right交换
if (left <= right)
Swap(&arr[left++], &arr[right--]);
}
//right的位置就是基准值的位置
Swap(&arr[keyi], &arr[right]);
return right;
}
2.3.2.2.挖坑法
思路 :
创建左右指针。首先从右向左找出比基准值小的数据,找到后立即放入左边坑中,当前位置变为新的"坑",然后从左向右找出比基准值大的数据,找到后立即放入右边的坑中,当前位置变为新的"坑",结束循环后将最开始存储的分界值放入当前的"坑"中,返回当前"坑"的下标(即分界值下标)
c
//找基准值 挖坑法
int _QuickSort2(int* arr, int left, int right)
{
int hole = left;
int key = arr[hole];
while (left < right)
{
while (left < right && arr[right] > key)
{
right--;
}
arr[hole] = arr[right];
hole = right;
while (left < right && arr[left] < key)
{
++left;
}
arr[hole] = arr[left];
hole = left;
}
arr[hole] = key;
return hole;
}
2.3.2.3.lomuto前后指针
创建前后指针,从左往右找比基准值小的进行交换,使得小的都排在基准值的左边
c
//找基准值 lumoto双指针法
int _QuickSort(int* arr, int left, int right)
{
int prev = left, cur = prev + 1;
int keyi = left;
while (cur <= right)
{
//cur数据和基准值比较
if (arr[cur] < arr[keyi] && ++prev != cur)
{
Swap(&arr[cur], &arr[prev]);
}
++cur;
}
Swap(&arr[keyi], &arr[prev]);
return prev;
}
快速排序的特性 :
①:时间复杂度:O(nlogn)
②:空间复杂度:O(logn)
2.3.2.4.非递归版本
非递归版本的快速排序需要借助数据结构:栈
c
//非递归版本快速排序----栈
void QuickSortNorR(int* arr, 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);
//[begin,end]-----找基准值
int keyi = begin;
int prev = begin, cur = prev + 1;
while (cur <= end)
{
if (arr[cur] < arr[keyi] && ++prev != cur)
{
Swap(&arr[prev], &arr[cur]);
}
++cur;
}
Swap(&arr[prev], &arr[keyi]);
keyi = prev;
//begin keyi end
//左序列[begin,keyi-1]
//右序列[keyi+1,end]
if (keyi + 1 < end)
{
STPush(&st, end);
STPush(&st, keyi + 1);
}
if (begin < keyi - 1)
{
STPush(&st, keyi - 1);
STPush(&st, begin);
}
}
STDesTroy(&st);
}
2.4.归并排序
归并排序算法思想:
归并排序是建立在归并操作上的一种有效的排序算法,该算法是采用分治法的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列,即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为二路归并。归并排序核心步骤:
c
void _MergeSort(int* arr, int left, int right,int* tmp)
{
//分解
if (left >= right)//==也可以
{
return;
}
int mid = (left + right) / 2;
//根据mid将[left,right]划分左右两个序列:[left,mid] [mid+1,right]
_MergeSort(arr, left, mid, tmp);
_MergeSort(arr, mid + 1, right, tmp);
//合并两个序列:[left,mid] [mid+1,right]
int begin1 = left, end1 = mid;
int begin2 = mid + 1, end2 = right;
int index = left;
while (begin1 <= end1 && begin2 <= end2)
{
if (arr[begin1] < arr[begin2])
{
tmp[index++] = arr[begin1++];
}
else {
tmp[index++] = arr[begin2++];
}
}
//要么begin1序列中数据没有放完
//要么begin2序列中数据没有放完
while (begin1 <= end1)
{
tmp[index++] = arr[begin1++];
}
while (begin2 <= end2)
{
tmp[index++] = arr[begin2++];
}
//tmp--》arr
for (int i = left; i <= right; i++)
{
arr[i] = tmp[i];
}
}
//归并排序
void MergeSort(int* arr, int n)
{
int* tmp = (int*)malloc(sizeof(int) * n);
//[0,n-1]
_MergeSort(arr, 0, n - 1,tmp);
free(tmp);
}
归并排序的特性 :
①:时间复杂度:O(nlogn)
②:空间复杂度:O(n)
3.排序算法复杂度及稳定性分析
稳定性 :假定在待排序的记录序列中,存在多个具有相同的关键字的记录,若经过排序,这些记录的相对次序保持不变,即在原序列中,r[i]=r[j],且r[i]在r[j]之前,而在排序后的序列中,这两个元素的相对位置没有发生改变,则称这种排序算法使稳定的,否则称之不稳定。
稳定性验证案例 :
直接选择排序 :5 8 5 2 9
希尔排序 :5 8 5 2 9
堆排序 :2 2 2 2
快速排序:5 3 3 4 3 8 9 10 11