数据结构六大排序详解(插入排序、希尔排序、选择排序、冒泡排序、快速排序)
1、排序的概念
1、排序的概念
- 排序:所谓排序,就是使一串记录,按照其中的某个或某些关键字的大小,递增或递减的排列起来的操作。
- 稳定性:假定在待排序的记录序列中,存在多个具有相同的关键字的记录,若经过排序,这些记录的相对次序保持不变,即在原序列中,r[i]=r[j],且r[i]在r[j]之前,而在排序后的序列中,r[i]仍在r[j]之前,则称这种排序算法是稳定的;否则称为不稳定的。
- 内部排序:数据元素全部放在内存中的排序。
- 外部排序:数据元素太多不能同时放在内存中,根据排序过程的要求不能在内外存之间移动数据的排序。
2、常见的排序算法
2、数据结构五大排序
2.1 插入排序
1、基本思想:
- 直接插入排序是一种简单的插入排序法,其基本思想是:把待排序的记录按其关键码值的大小逐个插入到一个已经排好序的有序序列中,直到所有记录插入完为止,得到一个新的有序序列。
- 实际中我们玩扑克牌的时候,就用了插入排序的思想。
2、基本过程
- 假如第一个数已经排好序
- 第一个数的后一个数记为temp,将temp与第一个数进行比较,如果小于第一个数,则第一个数与temp交换位置
- 再将第三个数记为temp,temp依次向前比较,若小于前面的数,则交换位置,若大于前面的位置,则保持不动,temp指向下一个值
- 后面的数和前面的比较方法相同,直到整个序列有序为止
图解:
代码实现:
void InsertSort(int *arr, int n)
{
for (int i = 0; i < n - 1; ++i)
{
int end = i;//end有序数列最后一个元素的下标
int temp = arr[end + 1];//temp待插入的元素
while (end >= 0)
{
if (temp <= arr[end])
{
arr[end + 1] = arr[end];
--end;
}
else
break;
}
arr[end + 1] = temp;
}
}
总结:
1、再待排序的元素当中,假设前n-1个元素都是已经排好序的,然后将第n个元素插入到已经排好的序列当中,使得前n个元素有序,按照此种方法将数据进行插入,直到完全有序。
2、在此排序中我们在所有数据没有完全插入以前,并不能确定每个数据的位置,直到所有数据完全插入。
3、元素集合越接近有序,插入排序算法的时间效率就越高
4、时间复杂度:O(N^2)
5、空间复杂度:O(1),它是一种稳定的排序算法
6、稳定性:稳定
2.2 希尔排序
1、基本思想
希尔排序法又称缩小增量法。希尔排序法的基本思想是:先选定一个整数n,把待排序文件中所有记录每隔n进行分组,并将距离为n的元素分为一组,然后再进行插入排序,在取一个比小的数,重复上述操作,直到n等于1为止。
2、基本步骤
- 先选定一个小于N的整数gap作为第一增量,然后将所有距离为gap的元素分在同一组,并对每一组的元素进行直接插入排序,然后再取一个比第一个增量小的整数作为第二增量,再重复上述操作…
- 当增量的大小减到1时,就相当于整个数列被分到了一组,再进行一次直接插入排序,排序完成。
图解
代码实现:
void ShellSort(int *arr, int n)
{
int gap = n;
while (gap > 1)
{
//每次将gap缩小一半
gap /= 2;
for (int i = 0; i < n-gap; ++i)
{
int end = i;
int temp = arr[end + gap];
while (end >= 0)
{
if (temp < arr[end])
{
arr[end + gap] = arr[end];
end-=gap;
}
else
break;
}
arr[end+gap] = temp;
}
}
}
特性总结:
1、希尔排序是对直接插入排序的优化。
2、当gap>1时都是预排序,目的是让数组更接近于有序。当gap==1时,数组已经接近于有序了,这样就会很快。这样整体而言,可以达到优化的效果。我们是实现后可以进行性能测试的比对。
3、希尔排序的时间复杂度不好计算,需要进行推导,对导出来平均时间复杂度:O(N^1.3—N ^2)
4、稳定性:不稳定
2.3 选择排序
1、基本思想
每次从待排序的数据元素中选出最大(或最小)的一个元素,存放子序列的起始位置,知道待排序的数据元素排完。实际上,我们可以一趟选出两个值,一个最大值一个最小值,然后将其放在序列开头和末尾,这样可以使选择排序的效率快一倍。
2、基本过程
- 在元素集合array[i]–array[n-1]中选择关键码最大(小)的数据元素
- 若他不是这组元素中的最后一个元素,则将它与这组元素中的最后一个(第一个)元素交换
- 在剩余的array[i]–array[n-2](array[i-1]–array[n-1])集合中,重复上述步骤,直到集合剩余1个元素
图解:
代码实现:(此处是先选择最大的和最小的排在开头和结尾可以将效率提高一倍)
void SelectSort(int *arr, int n)
{
//参与单趟排序的第一个数和最后一个数的下标
int begin = 0, end = n - 1;
while (begin < end)
{
int max = begin;//max最大值的下标
int min = begin;//min最大值的下标
for (int i = begin; i <= end; ++i)
{
if (arr[i]>arr[max])
max = i;
if (arr[i] < arr[min])
min = i;
}
swap(&arr[min], &arr[begin]);//最小值放在开头
if (begin == max)//防止最大的数在begin位置被换走
max = min;
swap(&arr[max], &arr[end]);
++begin;
--end;
}
}
特性总结
-
直接选择排序思路非常好理解,但是效率不是很好,实际当中很少使用
-
时间复杂度:O(N^2)
-
空间复杂度:O(1)
-
稳定性:不稳定
2.4 冒泡排序
1、基本思想:将最大的数依次向后移动,第一趟下来最大的在最左边。
2、基本过程:
-
假设一共有M个元素需要排序,比较相邻的元素。如果第一个比第二个大(或小),则进行交换。
-
按照既定顺序,对每一对相邻元素做同样的工作,从开始第一对到结尾的最后一对。第一轮比较完成以后,最后的元素应该会是整个元素列中最大(或最小)的数。
-
第N(N<M)轮比较结束后,会有N个元素已经被放置在正确的顺序位置,即每轮排序的最后一个元素,针对所有的剩余的元素(M-N)重复以上的步骤。
-
持续每次对越来越少的元素重复上面的步骤,直到没有任何一对数字需要比较。
图解
代码实现
void BubbleSort(int *arr, int n)
{
int end = n;
while (end)
{
int flag = 0;
for (int i = 1; i < end; ++i)
{
if (arr[i - 1]>arr[i])
{
swap(&arr[i], &arr[i - 1]);
flag = 1;
}
}
if(flag==0) break;
--end;
}
}
特性总结
- 冒泡排序是一种非常容易理解的排序
- 时间复杂度:O(N^2)
- 空间复杂度:O(1)
- 稳定性:稳定
2.5 快速排序
快速排序是Hoare与1962年提出的一种二叉树结构的交换排序方法,其基本思想为:任取待排序元素中的某元素作为基准值,按照该排序码将待排序集合分割成两个子序列,左子序列中所有元素均小于基准值,右子序列中所有元素均大于基准值,然后对左右子序列重复这个过程,直到所有元素都排列在相应位置上为止。
将区间按照基准值划分为左右两半部分的常见、方式有:
- hoare版本
- 挖坑法
- 前后指针版本
2.5.1 hoare版本(左右指针法)
1、基本过程
- 先选出一个key,基本上都会选择最左边或者最右边
- 再定义一个左指针begin和一个右指针end,左指针从左向右走,右指针从右向左走(需要注意的是:若选择最左边的数据作为key,则需要end先走,若选择最右边的数据作为key,则需要begin先走)
- 先选取最左边的数作为key,在走的过程中,若end遇到小于key,则停下,begin开始走,直到begin遇到一个大于key的数时,将begin和end交换位置,end再继续走,如此进行下去,直到begin和end最终相遇,此时将相遇点的内容与key进行交换
- key交换一次以后,key左边的数都是小于key,key右边的数都是大于key
- 再将key左右的数进行同样的排序,如此反复下去,直到左右只有一个数或者左右数不存在时则排序完成。
图解:(图解选择右边作为key,左边同理)
代码实现:
void QuickSort(int *arr, int begin, int end)
{
//若只存在一个数,则区间不存在
if (begin >= end)
return;
int left = begin, right = end-1;
int key = begin;
while (begin < end)
{
while (end>begin && arr[end] >= arr[key])
--end;
while (end>begin && arr[begin] <= arr[key])
++begin;
swap(&arr[begin], &arr[end]);
}
swap(&arr[end], &arr[key]);
key = end;
QuickSort(arr, left, key - 1);
QuickSort(arr, key + 1, right);
}
2.5.2 挖坑法
挖坑法之递归实现
基本思路:
- 首先选取一个数的位置作为坑位,一般都是最左边的数,将坑位的数赋值给temp
- 设置两个指针,一个左指针一个右指针,右指针向左走寻找比坑位小的数,找到后停下来,左指针向右走,寻找比坑位数大的数,找到以后停下来,然后将来将左右指针的数交换位置,最后左右指针相遇的坑位就是temp的位置
- 此时temp左边的数都比temp小,右边的数都比temp大
- 左右在重复上述过程,指代左右只有一个数或者不存在时为止。
图解:
代码实现
void QuickSort1(int *arr, int begin, int end)
{
//若只存在一个数,则区间不存在
if (begin >= end)
return;
int left = begin, right = end;
int key = arr[begin];
while (begin < end)
{
while (end > begin && arr[end] >= key)
--end;//找小
arr[begin] = arr[end];//小的放左边的坑里
while (end > begin && arr[begin] <= key)
++begin;//找大
arr[end] = arr[begin];//大的放右边的坑里
}
arr[begin] = key;
int keyi = begin;
QuickSort1(arr, left, keyi - 1);
QuickSort1(arr, keyi + 1, right);
}
挖坑法之非递归实现
int PartSort(int *arr, int begin, int end)
{
int key = arr[begin];
while (begin < end)
{
while (key <= arr[end] && begin < end)
--end;
arr[begin] = arr[end];
while (key >= arr[begin] && begin < end)
++begin;
arr[end] = arr[begin];
}
arr[begin] = key;
int meeti = begin;
return meeti;
}
void QuickSort2(int *arr, int begin, int end)
{
stack<int>st;
st.push(end);
st.push(begin);
while (!st.empty())
{
int left = st.top();//左区间
st.pop();
int right = st.top();//右区间
st.pop();
int mid = PartSort(arr, left, right);
//当左区间>=mid-1则证明左区间已经排好序
if (left < mid - 1)
{
st.push(mid - 1);
st.push(left);
}
//当mid+1>=右区间则证明区间已将排好序
if (right>mid + 1)
{
st.push(right);
st.push(mid + 1);
}
}
}
2.5.3 前后指针法
基本思路
- 先选出一个key,一般是最左边或是最右边的。
- 指定prev指针指向待排序数列开头,cur指针指向prev+1。
- 若cur指向的数值小于key,则prev先向后移动一位,然后交换prev和cur指针指向的内容,然后再给cur指针++;
- 若cur指向的内容大于key,则cur指针直接++,如此进行下去,直到cur到达end位置,此时将key和++prev指针指向的内容交换即可。
- 经过一次单趟排序,最终也能使得key左边的数据全部都小于key,key右边的数据全部都大于key。
- 然后也还是将key的左序列和右序列再次进行这种单趟排序,如此反复操作下去,直到左右序列只有一个数据,或是左右序列不存在时,便停止操作
图解
代码实现
void QuickSort3(int *arr, int begin, int end)
{
if (begin >= end)
return;//防止区间不存在
int cur = begin, prev = begin - 1;
int key = end;
while (cur != key)
{
if (arr[cur] < arr[key] && ++prev != cur)
swap(&arr[cur], &arr[prev]);
++cur;
}
swap(&arr[++prev], &arr[key]);
key = prev;
QuickSort3(arr, begin, key - 1);
QuickSort3(arr, key + 1, end);
}
特性总结
- 快速排序整体的综合性能和使用场景都是比较好的,所以才会叫快速排序
- 时间复杂度:O(N*logN)
2.6 堆排序
堆排序可参见——堆排序详解