关于排序算法这一篇已经足够【建议收藏】
大鸣人镇楼
前言
本篇详细介绍了几种常见的算法,以及使用条件,场景,以及相关复杂度的问题,能熟练的掌握这几种算法,对于考试以及面试中有十分有必要的。
排序
1.有关于排序的概念
1.1排序:
所谓排序,就是使一串记录,按照其中的某个或某些关键字的大小,递增或递减的排列起来的操作。
1.2稳定性:
假定在待排序的记录序列中,存在多个具有相同的关键字的记录,若经过排序,这些记录的相对次序保持不变,即在原序列中,r[i]=r[j],且r[i]在r[j]之前,而在排序后的序列中,r[i]仍在r[j]之前,则称这种排序算法是稳定的;否则称为不稳定的。
1.3内部排序:
数据元素全部放在内存中的排序,称为内部排序。
1.4外部排序:
在实际中,经常需要对大文件进行排序,但是因为文件中的信息量比较庞大,无法将整个文件拷贝进内存进行排序,所以在排序过程中需要对外存进行访问的排序过程。
2.常见排序算法的实现
2.1插入排序
插入排序:
它的基本思想直接插入排序是一种简单的插入排序法,其基本思想是:把待排序的记录按其关键码值的大小逐个插入到一个已经排好序的有序序列中,直到所有的记录插入完为止,得到一个新的有序序列。
2.1.1直接插入排序。
直接插入排序具体思路:
当插入第i(i>=1)个元素时,前面的array[0],array[1,…,array[i-1]已经排好序,此时用array[i]的排序码与array[i-1],array[i-2],…的排序码顺序进行比较,找到插入位置即将array[i]插入,原来位置上的元素顺序后移。
思维图:
代码部分
void InsertSort(int arr[], int size)
{
//表示当前要插入的元素在数组中的下标
//i位置的元素一定是往i之前插入的
for (int i = 1; i < size; i++)
{ //找插入的位置 往i位置之前找,i
//之前的位置都是有序的
int end = i - 1;
//保存i的元素
int key = arr[i];
//end往前
while (end >= 0 && key < arr[end])
{ //小于之前的元素将之前的元素往后搬移
arr[end + 1] = arr[end];
end--;
}
//插入元素
arr[end + 1] = key;
}
}
1.时间复杂度:O(N^2);
2.空间复杂度:O(1);
3.稳定性:稳定
4.应用场景:元素越接近有序,或者元素最少的情况,越接近有序,直接插入排序算法的时间效率越高。
2.1.2希尔排序
直接插入法解决了少量的有序的元素,但是如果是量大杂乱的数据?仍然使用插入排序的思想?这时我们引入了希尔排序。
希尔排序:
希尔排序法又称缩小增量法。希尔排序法的基本思想是:先选定一个整数,把待排序文件中所有记录分成个组,所有距离为的记录分在同一组内,并对每一组内的记录进行排序。然后,取,重复上述分组和排序的工作。当到达=1时,所有记录在统一组内排好序。
通俗来讲就是通过分组将数据降下来,给定gap,下标模完gap相同的放在一组,进行插入排序,
然后gap-1对上面接近有序的数组继续进行排序,到gap=1时,排序结束。
接近有序的概念:小的数据尽量靠前,大的数据尽量靠后,不大不小的尽量放中间。
void ShellSort(int arr[], int size)
{
int gap = size;
while (gap > 1)
{
gap = (gap / 3 )+ 1;
for (int i = gap; i < size; i++)//i++是将几个分组进行交替的排序
{ //找插入的位置 往i位置之前找,i
//之前的位置都是有序的
int end = i - gap;
//保存i的元素
int key = arr[i];
//end往前
while (end >= 0 && key < arr[end])
{ //小于之前的元素将之前的元素往后搬移
arr[end + gap] = arr[end];
end -= gap;
}
//插入元素
arr[end + gap] = key;
}
gap--;
}
}
gap = (gap / 3 )+ 1,这种取法的时间复杂度最为高效O(N^1.3).
时间复杂度:O(N^2);
空间复杂度:O(1);
稳定性:由于多次插入排序,元素可能在各自的插入的排列中移动,最后稳定性就会被打乱,所以是不稳定的。
2.2交换排序
2.2.1冒泡排序
冒泡排序的主要思想:
冒泡是将两两数据进行比较,将大的放在后面,第一趟将最大的数据放在最后,第二趟将第二大数据放在第一大的前面。按照这样的方法,直到走N-1趟,排序结束。
代码部分
void BubbleSort(int arr[], int size)
{
int flag = 0;
//控制冒泡的趟数,最后一趟只剩一个元素,因此一个少一趟
for (int i = 0; i < size - 1; i++)
{
int flag = 0;//判断是否有效的标志
//冒泡的方式
//用两个相邻的元素进行比较,不满足则交换
for (int j = 0; j < size - i - 1; j++)//j是比较两个元素的前一个,所以-1,
//保证后一个j+1不越界
{
if (arr[j] > arr[j + 1])
flag = 1;
Swap(&arr[j],&arr[j + 1]);
}
if (!flag)//每一躺,如果没有交换则有序。
{
return;
}
}
}
时间复杂度:O(N^2)
空间复杂度:O(1)
稳定性:冒泡排序是将相邻的两个元素进行比较,交换也发生在这两个元素之间,所以冒泡排序是一种稳定的排序。
2.2.2快速排序
快速排序:
是Hoare于1962年提出的一种二叉树结构的交换排序方法,其基本思想为:
任取待排序元素序列中的某元素作为基准值,按照该排序码将待排序集合分割成两子序列,左子序列中所有元素均小于基准值,右子序列中所有元素均大于基准值,然后最左右子序列重复该过程,直到所有元素都排列在相应位置上为止。
重点:运用递归的思想去排左右区间
将区间按照基准值划分为左右两半部分的常见方式有:
1 hoare版本
2 挖坑法
3.前后指针
快速排序的优化
1.三数取中法
降低每次取基准值取到极端值的概率,降低了时间复杂度
代码部分
int GetMid(int arr[], int left, int right)
{
int mid = left + ((right - left) >> 1);
if (arr[left] < arr[right - 1])
{
if (arr[mid] < arr[left])//中间值为最左侧的数据
return left;
else if (arr[mid]>arr[right - 1])
return right - 1;//中间值为最右侧的数据
else
return mid;
}//arr[left]>arr[right-1]
else
{
if (arr[mid] > arr[left])
return left;
else if (arr[mid] < arr[right - 1])
return right - 1;
else
return mid;`在这里插入代码片`
}
}
2.递归到小的子区间时,可以考虑用插入排序。
每次递归都是函数的调用,需要分配空间,都需要耗费空间,所以当剩余值较少时,采用插入排序,减少了递归调用的次数。
2.2.2.1快速排序递归实现
类似于二叉树的前序遍历.
代码部分
void Quicksort(int arr[], int left, int right)
{
int div = 0;
if (right - left <= 16)
{
InsertSort(arr+left, right-left);
}
else
{
//1.找基准值对区间中的数进行划分
//div表示划分好之后基准值的位置
div = Partion3(arr, left, right);
//[left,div)
Quicksort(arr, left, div);
//[div+1,right)
Quicksort(arr, div + 1, right);
}
}
时间复杂度:O(NlogN)
空间复杂度:O(logN)
稳定度:不稳定
2.2.2.2快速排序的循环实现
循环实现与递归原理类似,不过是利用栈,将数据压入栈中,在利用分割区间的思想,完成排序。
代码部分
void QuicksortN(int arr[], int size)
{
stack s;
StackInit(&s);
int left = 0;
int right = size;
StackPush(&s, left);
StackPush(&s, right);
while (!StackEmpty(&s))
{
right = StackTop(&s);
StackPop(&s);
left = StackTop(&s);
StackPop(&s);
if (right - left > 1)
{
int div = Partion2(arr, left, right);
//[div,right)
StackPush(&s, div);
StackPush(&s, right);
//[left,div)
StackPush(&s, left);
StackPush(&s, div);
}
}
StackDestroy(&s);
}
2.3选择排序
1.基本思想:
每一次从待排序的数据元素中选出最小(或最大)的一个元素,存放在序列的起始位置,直到全部待排序的数据元素排完。
2.3.1直接选择排序
1.具体算法:
1.在元素集合array[i]–array[n-1]中选择关键码最大(小)的数据元素
2.若它不是这组元素中的最后一个(第一个)元素,则将它与这组元素中的最后一个(第一个)元素交换
3.在剩余的array[i]–array[n-2](array[i+1]–array[n-1])集合中,重复上述步骤,直到集合剩余1个元素。
代码实现:
void SelectSort(int arr[], int size)
{
for (int i = 0; i < size-1; ++i)
{
//在arr中找最大元素的位置,将其放在数组的末尾
int maxpos = 0;
for (int j = 1; j < size - i; j++)
{
if (arr[j]>arr[maxpos])
{
maxpos = j;
}
//最大位置的下标已经找到
if (maxpos != size - i - 1)
{
Swap(&arr[maxpos], &arr[size - i - 1]);
}
}
}
}
1.直接选择排序思考非常好理解,但是效率不是很好。
实际中很少使用
2.时间复杂度:O(N^2)
3. 空间复杂度:O(1)
4. 稳定性:不稳定
2.3.2堆排序
因为选择排序的缺陷是存在重复,为了优化,我们引进堆排序。
1.堆排序
堆排序(Heapsort)是指利用堆积树(堆)这种数据结构所设计的一种排序算法,它是选择排序的一种。它是通过堆来进行选择数据。
需要注意的是排升序要建大堆,排降序建小堆。
代码部分:
//向下调整法
void HeapAdjust(int arr[], int size, int parent)
{ //先标记左孩子
int child = 2 * parent + 1;
//左孩子存在
while (child < size)
{
if (child + 1 < size&&arr[child + 1] > arr[child])
child++;
if (arr[parent] < arr[child])
{
Swap(&arr[parent], &arr[child]);
parent =child;
child = 2 * parent + 1;
}
else
{
break;
}
}
}
//堆排序
void HeapSort(int arr[], int size)
{
int end = size - 1;
//建堆 升序建大堆 降序建小堆
//从倒数第一个非叶子节点的位置开始
//一直到根的位置应用向下调整
for (int root = ((size - 2) >> 1); root >= 0; root--)
{
HeapAdjust(arr, size, root);
}
//排序 利用堆删除的思想
while (end)
{
Swap(&arr[end], &arr[0]);
//交换之后,将堆顶位置的元素向下调整,size的位置减一为end
HeapAdjust(arr, end, 0);
end--;
}
}
时间复杂度:O(NlogN)
空间复杂度:O(1);
稳定度:不稳定
2.4归并排序
1.基本思想:
归并排序是建立在归并操作上的一种有效的排序算法,该算法是采用分治法的一个非常典型的应用。
将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为二路归并。
注意:归并排序的使用场景应用于数据量非常大,不能一次性加载到内存中。
先分割在归并,每次找到中间位置,均分成两部分,直到分割成一个元素,然后归并。
在完成归并排序前,我们首先要完成两个有序数组,合并称为一个数组。用于完成归并部分。
2.两个有序数组的合并
代码部分:
void MergeData(int arr[], int left, int mid, int right, int temp[])
{
int begin1 = left, end1 = mid;
int begin2 = mid , end2 = right;
int index = left;
while (begin1 < end1 && begin2 < end2)
{
if (arr[begin1] <= arr[begin2])
{
temp[index++] = arr[begin1++];
}
else
{
temp[index++] = arr[begin2++];
}
}
//1里面的没放完
while (begin1 < end1)
{
temp[index++] = arr[begin1++];
}
// 2里面的没放完
while (begin2 < end2)
{
temp[index++] = arr[begin2++];
}
}
3.归并排序的实现
代码部分:
void _MergeSort(int arr[], int left, int right, int temp[])
{
if (right - left <= 10)
{
InsertSort(arr + left, right - left);
}
else
{
int mid = left + ((right - left) >> 1);
//[left,mid)
_MergeSort(arr, left, mid, temp);
//[mid,right)
_MergeSort(arr, mid, right, temp);
//归并
MergeData(arr, left, mid, right, temp);
memcpy(arr + left, temp + left, sizeof(arr[0])* (right - left));
}
}
- 归并的缺点在于需要O(N)的空间复杂度,归并排序的思考更多的是解决在磁盘中的外排序问题。
- 时间复杂度:O(N*logN)
- 空间复杂度:O(N)
- 稳定性:稳定
2.5非比较排序
2.5.1计数排序
计数排序适用于序列在一定密集范围内的。
计数排序的步骤:
1.找数据的范围 ,找最小值,和最大值(如果没有告诉)
2.统计的结果得保存起来,申请保存计数得空间
3.统计每个元素出现的次数,
2.按照计数的结果数据,按照计数数组的下标从小往大回收。
代码部分:
void CounSort(int arr[], int size)
{
// 1.找数据的范围 ,找最小值,和最大值
int max = arr[0];
int min = arr[0];
for (int i = 1; i < size; i++)
{
if (arr[i] <min)
{
min = arr[i];
}
if (arr[i]>max)
{
max = arr[i];
}
}
//2.统计的结果得保存起来,申请保存计数得空间
int range = max - min + 1;
int* count = (int*)calloc(range, sizeof(count[0]));
if (NULL == count)
return;
//3.统计每个元素出现得次数
for (int i = 0; i < size; i++)
{
//count中存放的是每个数组出现的次数
count[(arr[i] - min)]++;
}
//4.0按照count数组的下标进行回收
int index = 0;//虽然是两个循环,但回收了n个数据,时间复杂度为o(n)
for (int i = 0; i < range; i++)
{
while (count[i]--)
{
arr[index++] = i + min;
}
}
free(count);
}
- 计数排序在数据范围集中时,效率很高,但是适用范围及场景有限。
- 时间复杂度:O(N+range)
- 空间复杂度:O(range)
- 稳定性:稳定
总结
将自己学习的知识进行总结,整理,在博客中见证自己的成长!!!