数据结构------排序(1)
文章目录
一、排序
1.概念:
所谓排序,就是使一串记录,按照其中的某个或某些关键字的大小,递增或递减的排列起来的操作。
2.运用:
购物筛选排序,院校排名等。
3.常见排序算法
二、实现排序算法
1.插入排序
1.1直接插入排序
基本思想:
把待排序 的记录按其关键码值的大小 逐个插入到一个已经排好序的有序序列中,直到所有的记录插入完为止,得到一个新的有序序列。
实际中我们玩扑克牌时就用了插入排序的思想。
动图理解:
当插⼊第 i(i>=1) 个元素时,前⾯的 array[0],array[1],...,array[i-1] 已经排好序,此时用 array[i] 的排序码与 array[i-1],array[i-2],... 的排序码顺序进⾏⽐较,找到插⼊位置即将 array[i] 插⼊,原来位置上的元素顺序后移。
代码实现:
C
void InsertSort(int* arr, int n)
{
for (int i = 0; i < n - 1; i++)
{
int end = i;//有序区间的最后一个数据
int tmp = arr[end + 1];//有序区间的后一个数据
while (end >= 0)
{
if (arr[end] > tmp)//当arr[end]>tmp,两值交换
{
arr[end + 1] = arr[end];//把tmp放在end的位置
end--; //end移动到有序区间的倒数第二个数据
}
else //当arr[end]<=tmp,不做处理,结束循环
{
break;
}
}
arr[end + 1] = tmp;
}
}
- 为什么for循环的判断条件是i<n-1,而不是i<n呢?
因为当end=n-1时,tmp=arr[n],这里越界了,所以for循环的判断条件是i<n-1。
- 时间复杂度
最差的情况(数组有序且为降序 的情况下时间复杂度最差):时间复杂度为O(N^2^)
最好的情况(数组有序且为升序的情况下时间复杂度最差):时间复杂度为O(N)。
- 空间复杂度:O(1)
1.2希尔排序
曾经我们学过冒泡排序,我们知道冒泡排序的算法效率极低(最差的时间复杂度为O(N^2^)),所以在实际工作中,我们不会用到它。即冒泡排序只有教学意义,没有实际意义。
直接插入排序有实际意义吗?
它有实际意义,但是要优化一下。当数组为降序序列时,直接插入排序还能得到优化吗?这里我们就要引出------希尔排序。
希尔排序法⼜称缩小增量法,是一种改进的插入排序算法。它通过比较相距一定间隔的元素来进行排序,逐步缩小间隔,最终间隔为1时,算法退化为普通的插入排序。希尔排序的名称来源于其发明者Donald Shell。
基本思想:
希尔排序法的基本思想是:先选定⼀个整数(通常是gap = n/3+1),把待排序⽂件所有记录分成各组,所有的距离相等的记录分在同⼀组内,并对每⼀组内的记录进⾏排序,然后gap=gap/3+1得到下⼀个整数,再将数组分成各组,进⾏插⼊排序,当gap=1时,就相当于直接插⼊排序。
以排序数组为例(gap取2)
代码实现:
C
void ShellSort(int* arr, int n)
{
int gap = n;//6//gap表示我们要分多少组
while (gap > 1)
{
gap = gap / 3 + 1;//6除3=2,2除以3=0 //+1是为了保证最后一次gap一定为1
for (int i = 0; i < n - gap; i ++)
{
int end = i;
int tmp = arr[end + gap];//tmp最后一个取值要保证不越界
while (end >= 0)
{
if (arr[end] > tmp)
{
arr[end + gap] = arr[end];
end -= gap;
}
else
{
break;
}
}
arr[end + gap] = tmp;
}
}
}
时间复杂度分析:
外层循环:
外层循环的时间复杂度可以直接给出为: O (log2 n ) 或者 O (log3 n ) ,即 O (log n)
内层循环:
假设⼀共有n个数据,合计gap组,则每组为n/gap(大致)个;在每组中,插⼊移动的次数最坏的情况下为 S=1 + 2 + 3 + ......+ (n/gap-1),⼀共是gap组,因此:总计最坏情况下移动总数为:gap ∗ S
gap取值有(以除3为例):n/3 n/9 n/27 ... 2 1
一 一带入:
当gap为n/3时,移动总数为: n
当gap为n/9时,移动总数为: 4n
最后⼀趟,数组已经已基本有序了,gap=1即直接插入排序,移动次数就是n
通过以上的分析,可以画出这样的曲线图:
因此,希尔排序在最初和最后的排序的次数都为n,即前⼀阶段排序次数是逐渐上升的状态,当到达某⼀顶点时,排序次数逐渐下降⾄n,⽽该顶点的计算暂时⽆法给出具体的计算过程。希尔排序时间复杂度不好计算,因为 gap 的取值很多,导致很难去计算,因此很多书中给出的希尔排序的时间复杂度都不固定。《数据结构(C语⾔版)》--- 严蔚敏书中给出的时间复杂度为:
总而言之,希尔排序的时间复杂度范围为:O(N^1.3^)~O(N^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;
int end=n-1;
while(begin<end)
{
int mini=begin,maxi=begin;
for(int i=begin+1;i<=end;i++)
{
if(arr[i]>arr[maxi])
{
maxi=i;
}
if(arr[i]<arr[mini])
{
mini=i;
}
}
//避免maxi begin都在同一个位置,begin和mini交换之后,mini数据变成了最小的数据
if(maxi==begin)
{
maxi=mini;
}
Swap(&arr[mini],&arr[begin]);
Swap(&arr[maxi],&arr[end]);
++begin;
--end;
}
}
- 时间复杂度是O(n^2^).
2.2堆排序
堆排序是指利⽤堆积树(堆)这种数据结构所设计的⼀种排序算法,它是选择排序的⼀种。它是通过堆来进行选择数据。需要注意的是排升序要建⼤堆,排降序建小堆。在上篇二叉树(下)中我们已经实现过堆排序,这⾥不再细述。
3.交换排序
3.1冒泡排序
冒泡排序是⼀种最基础的交换排序。之所以叫做冒泡排序,因为每⼀个元素都可以像小气泡⼀样,根据自身大小⼀点⼀点向数组的⼀侧移动。
动图理解:
代码实现:
C
void BubbleSort(int* arr, int n)
{
for (int i = 0; i < n; i++)
{
int exchange = 0;
for (int j = 0; j < n - i - 1; j++)
{
//升序
if (arr[j] > arr[j + 1])
{
exchange = 1;
Swap(&arr[j], &arr[j + 1]);
}
}
if (exchange == 0)
{
break;
}
}
}
- 时间复杂度(最差):O(N^2^).
3.2快速排序(采用二叉树递归的思想)
快速排序是Hoare于1962年提出的⼀种⼆叉树结构的交换排序⽅法。
基本思想:
任取待排序元素序列中的某元素作为基准值,按照该排序码将待排序集合分割成两子序列,左⼦序列中所有元素均小于基准值,右子序列中所有元素均大于基准值,然后最左右子序列重复该过程,直到所有元素都排列在相应位置上为止。
C
void QuickSort(int* arr, int left, int right)
{
if (left >= right)
{
return;
}
//[left,right]--->找基准值mid
int keyi = _QuickSort(arr, left, right);
//左子序列:[left,ley-1]
QuickSort(arr, left, keyi - 1);
//右子序列:[keyi+1,right]
QuickSort(arr, keyi + 1, right);
}
- 空间复杂度:O(logn)
- 时间复杂度:O(nlogn)
找基准值的三种方法
- hoare版本
算法思路 :
1)创建左右指针,确定基准值;
2)从右向左找出⽐基准值小的数据,从左向右找出比基准值大的数据,左右指针数据交换,进⼊下次循环.
问题1:为什么跳出循环后right位置的值⼀定不⼤于key?
当 left > right 时,即right⾛到left的左侧,⽽left扫描过的数据均不⼤于key,因此right此时指向的数据⼀定不⼤于key
问题2:为什么left 和 right指定的数据和key值相等时也要交换?
相等的值参与交换确实有⼀些额外消耗。实际还有各种复杂的场景,假设数组中的数据⼤量重复时,无法进⾏有效的分割排序。
C
int _QuickSort1(int* arr, int left, int right)
{
int keyi = left;
++left;
while (left<=right)//left和right相遇的位置的值比基准值要大
{
while (left <= right && arr[right] > arr[keyi])
{
right--;
}
//right找到比基准值小或等于
while (left <= right && arr[left] < arr[keyi])
{
left++;
}
if (left <= right)
{
Swap(&arr[left++], &arr[right--]);
}
}
Swap(&arr[keyi], &arr[right]);
return right;
}
- 挖坑法
创建左右指针。⾸先从右向左找出⽐基准⼩的数据,找到后⽴即放⼊左边坑中,当前位置变为新的"坑",然后从左向右找出⽐基准⼤的数据,找到后⽴即放⼊右边坑中,当前位置变为新的"坑",结束循环后将最开始存储的分界值放⼊当前的"坑"中,返回当前"坑"下标(即分界值下标)
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;
}
-
lomuto前后指针法
创建前后指针,从左往右找比基准值小的进行交换,使得小的都排在基准值的左边。
C
int _QuickSort3(int* arr, int left, int right)
{
int prev = left, cur = left + 1;
int keyi = left;
while (cur<=right)
{
if (arr[cur] < arr[keyi] && ++prev != cur)
{
Swap(&arr[cur], &arr[prev]);
}
cur++;
}
Swap(&arr[keyi], &arr[prev]);
return prev;
}