[数据结构] 八大排序,快进来学习了
Posted 哦哦呵呵
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了[数据结构] 八大排序,快进来学习了相关的知识,希望对你有一定的参考价值。
目录
以下的排序方式,都将按照升序方式进行排列。
用例为:
int arr[] = { 2, 1, 4, 6, 7, 0, 5, 9, 3, 8 };
基本概念:
稳定性:针对记录中相同的数据,在排序前后的顺序是否一致。
内部排序:所有数据一次加载到内存中
外部排序:不需要将所有数据加载到内存中
一. 插入排序
基本思想:将待排序列的记录按照其元素的大小逐个插入到一个已经排好序的有序序列中,直到所有记录插入完成,得到一个新的序列。
1. 直接插入排序
实现原理
- 找到待插入元素在集合中的位置
对于一个无序数组,从前向后进行排序,每一个元素都在其前方寻找插入位置,因为前方数据已经通过插入排序变成了有序序列。- 插入元素
图解
时间空间复杂度
时间复杂度:最坏情况下,序列为降序需要排升序复杂度为
O(n^2)
,最好情况下序列基本有序O(n)
空间复杂度:没有借助辅助空间,O(1)
稳定性 稳定
应用场景
根据时间复杂度可知,该排序适用于数据较少,或序列基本有序的情况下。
代码实现
// 插入排序
// 对于一个无序序列,选定一个元素,从前开始向后寻找插入位置,同时移动数组内元素
void InsertSort(int* arr, int n)
{
int i;
for (i = 1; i < n; i++)
{
int key = arr[i];
int end = i - 1;
// 寻找插入位置,并且将元素向后进行搬移
while (end >= 0 && arr[end] > key)
{
arr[end + 1] = arr[end];
end--;
}
// 插入元素
arr[end + 1] = key;
}
}
2. 希尔排序
实现原理
如果数据量过大,则直接插入排序就不能够使用了。但是还想利用插入排序的思想进行排序,则就可以使用希尔排序。
具体实现思想
希尔排序又称缩小增量法。
- 给定分组大小,例如给定分组值 gap=3
gap值我们一般给定 gap = gap / 3 + 1- 根据 gap 作为步长,每次加上gap值进行跳转,对组内的数据进行插入排序,使组内元素先有序,这样在最后排序时,组内元素就相对有序,搬移的元素次数也就少了很多
- 不断缩小gap,当gap=1时,序列有序。
图解
时间空间复杂度
时间复杂度:与gap取值有关,所以无法准确计算。
空间复杂度:没有借助辅助空间O(1)
稳定性 不稳定
代码实现
// 希尔排序
// 使用插入排序的思想,进行排序
// 插入排序适合数据量小并且数据接近有序的情况,并不适合数据量巨大,并且杂乱的情况
// 思想:
// 将数据进行分组处理,对每一组数据分别进行插入排序,开始时先指定分组间隔,后续不断缩小间隔进行分组排序
void ShellSort(int* arr, int n)
{
int gap = n;
int i;
// 按照不同间隔进行插入排序
while (gap > 1)
{
gap = gap / 3 + 1;
// 按照gap对元素进行分组
for (i = gap; i < n; i++)
{
int end = i - gap;
int key = arr[i];
// 查找插入位置
while (end >= 0 && arr[end] > key)
{
arr[end + gap] = arr[end];
end -= gap;
}
arr[end + gap] = key;
}
}
}
二. 选择排序
基本思想:每一次从待排序的序列中选出最大或者最小的元素,放在序列开始或者末尾的位置,直至所有元素排序完成。
1. 直接选择排序
实现原理
在待排序列中寻找最大值,找到之后放在待排序列末尾,不断缩小待排序列,直到序列中只剩余一个元素。
在寻找最大值的同时也可以同时寻找最小值,将最大最小值放在序列头尾,可以提高此算法效率。
图解
时间空间复杂度
时间复杂度:
O(n^2)
空间复杂度:O(1)
稳定性 不稳定
代码实现
// 在序列中寻找最大值,找到之后放在序列末尾,逐渐缩小该序列
void SelectSort(int* arr, int n)
{
int i, j;
for (i = 0; i < n - 1; i++)
{
int maxPos = 0;
// 寻找最大值, 记录位置i
// 每次寻找完后,都会与序列最后的元素进行交换,所以寻找最大值时,不需要寻找已经在队列最后的元素
for (j = 1; j < n - i; j++)
{
if (arr[maxPos] < arr[j])
{
maxPos = j;
}
}
// 将所找最大值放在序列末尾
if (arr[maxPos] != arr[n - i - 1])
{
Swap(&arr[maxPos], &arr[n - i - 1]);
}
}
}
// 选择排序的升级:在每次寻找最大值的同时,寻找序列中的最小值
// 最大值放在序列末尾,最小值放在序列头,每次循环后缩小查找范围
void SelectSortOP(int* arr, int n)
{
int end = n - 1;
int begin = 0;
while (begin < end)
{
int maxPos = begin;
int minPos = begin;
int index = begin + 1;
while (index < end)
{
if (arr[maxPos] < arr[index])
{
maxPos = index;
}
if (arr[minPos] > arr[index])
{
minPos = index;
}
index++;
}
if (maxPos != end)
{
Swap(&arr[maxPos], &arr[end]);
}
// 如果最小值的位置在end 上述交换完成后,会影响接下来的交换
if (minPos == end)
{
minPos = maxPos;
}
if (minPos != begin)
{
Swap(&arr[minPos], &arr[begin]);
}
end--;
begin++;
}
}
2. 堆排序
对二叉树堆存储方式不了解的话可以点击此链接进行查看 树与二叉树。
实现原理
根据二叉树堆的性质进行排序
- 建堆 排升序建大堆,排降序建小堆。建堆时利用向下调整算法进行调整,从倒数第一个非叶子结点的位置开始一直调整到根结点。
- 利用堆删除的思想进行排序。
图解
此用例为:int arr[] = {1, 2, 5, 3, 7, 8}
时间空间复杂度
时间复杂度:
O(log2 n)
空间复杂度:O(1)
稳定性 稳定
代码实现
void AdjustDown(int* arr, int n, int root)
{
int child = root * 2 + 1;
while (child < n)
{
// 先寻找根节点的两个孩子那个比较小
if (child + 1 < n && arr[child] < arr[child + 1])
{
child += 1;
}
// 如果根节点小于孩子 则进行交换
if (arr[root] < arr[child])
{
Swap(&arr[root], &arr[child]);
// 交换后给结点可能还是不满足堆特性,继续对该结点进行交换
root = child;
child = root * 2 + 1;
}
else
{
return;
}
}
}
// 堆排序
// 先利用向下调整算法,调整堆为大堆或小堆,之后按照删除堆元素的思想进行排序
// 默认排升序 建大堆
void HeapSort(int* arr, int n)
{
// 建堆
int i;
for (i = (n - 2) / 2; i >= 0; i--)
{
AdjustDown(arr, n, i);
}
// 利用堆删除思想进行排序
int end = n - 1;
while (end > 0)
{
Swap(&arr[0], &arr[end]);
AdjustDown(arr, end, 0);
end--;
}
}
三. 交换排序
基本思想:根据序列中两个元素值得比较结果来决定是否交换两个元素在序列中的位置,交换排序的特点:将键值较大的记录向序列尾部移动,键值较小的元素向前移动。
1. 冒泡排序
实现原理
逐个比较相邻两元素值,如果满足则交换两值的位置,直至每个元素比较完成。 每次排序都可以确定下来元素最大的位置。
图解
时间空间复杂度
时间复杂度:
O(n^2)
空间复杂度:O(1)
稳定性 稳定
代码实现
void BubbleSort(int* arr, int n)
{
int i, j;
for (i = 0; i < n - 1; i++)
{
int flag = 0;
for (j = 0; j < n - i - 1; j++)
{
if (arr[j] > arr[j + 1])
{
flag = 1;
Swap(&arr[j], &arr[j + 1]);
}
}
if (0 == flag)
{
break;
}
}
}
2. 快速排序
快排思想:在数据序列中,随便找一个基准值,按照该基准值划分为两个部分,小于基准值的放在左部,大于基准值的放在右部。先对左部递归上述操作,在对右部递归完成上述操作。递归完成后,就变成了有序序列。
在递归时每次都需要将序列不断划分为两个部分,所以按照何种方式进行划分序列, 并且如何让左右序列有序,就使用到了以下三种方法。
快排递归代码
// 在数据序列中,随便找一个基准值,将小于基准的放在一侧,大于基准值的放在另一侧
void QuickSortHelper(int* arr, int left, int right)
{
// 如果只剩余一个元素则不需要排序
if (right - left <= 1)
{
return;
}
int div = PartSort3(arr, left, right);
QuickSortHelper(arr, left, div);
QuickSortHelper(arr, div + 1, right);
}
2.1 hoare法
实现原理
采用双指针的防止,对区间左右各放置一个指针,左侧指针查找大于基准值的元素,右侧指针查找小于基准值的元素,当两指针都找到对应元素之后,交换两指针的元素。直到两指针相遇,停止交换。
在上述操作开始前,将基准值放在序列末尾,在两指针相遇后,再将基准值与相遇位置进行交换
图解
时间空间复杂度
时间复杂度:
O(n)
相当于遍历了一遍数组
代码实现
// 单个分割的方法时间复杂度:O(N)
int Partion1(int array[], int left, int right)
{
int begin = left;
int end = right - 1;
int mid = GetMiddleIndex(array, left, right);
Swap(&array[mid], &array[right - 1]);
int key = array[right-1];
while (begin < end)
{
// 让begin从前往后找,找比基准值大的元素
while (begin < end && key >= array[begin])
begin++;
// 让end从后往前找,找比基准值小的元素
while (begin < end && key <= array[end])
end--;
if (begin < end)
{
Swap(&array[begin], &array[end]);
}
}
if (begin != right-1)
{
Swap(&array[begin], &array[right - 1]);
}
return begin;
}
2.2 挖坑法
实现原理
与hoare方法基本相同,同样采用双指针的方式,左指针寻找比基准值大的元素,右指针寻找比基准值小的元素。与上述不同的是,在找到比基准值大或小的值之后,不等待继续查找使两个指针同时满足条件,而是立即将end指针或begin指针值填入该位置。
图解
代码实现
// 挖坑法
// 与上述结构相似,分别从左右寻找合适的值,找到之后不进行两指针的交换,而是将begin的值放在end位置,相互补充
int PartSort2(int* arr, int left, int right)
{
int begin = left;
int end = right;
int midPos = GetMidIndex(arr, left, right);
Swap(&arr[midPos], &arr[right]);
int key = arr[right];
while (begin < end)
{
// 寻找挖坑位置
while (begin < end && arr[begin] <= key)
{
begin++;
}
// begin变成了坑
if (begin < end)
{
arr[end] = arr[begin];
}
while (begin < end && arr[end] >= key)
{
end--;
}
// 填上begin的坑, end变成了坑
if (begin < end)
{
arr[begin] = arr[end];
}
}
// 填上最后的坑
arr[begin] = key;
return begin;
}
2.3 前后指针法
图解
代码实现
// 前后指针法
// 画图理解
int PartSort3(int* arr, int left, int right)
{
int cur = left;
int prev = cur - 1;
int midPos = GetMidIndex(arr, left, right);
Swap(&arr[midPos], &arr[right]);
int key = arr[right];
while (cur < right)
{
if (arr[cur] < key && ++prev != cur)
{
Swap(&arr[cur], &arr[prev]);
}
cur++;
}
Swap(&arr[prev + 1], &arr[right]);
return prev;
}
3. 快排的非递归实现
实现原理
递归转为非递归,一般使用的都是栈的方式,通过将待排区间进行入栈出栈,左右区间交叉排序。由于这种方法较好理解,就不再画图解释。
代码实现
// 利用栈的性质将递归转换为循环方式
// 将区间下标进行入栈,先入左边下标,再入右边下标
void QuickSortNonRHelper(int* arr, int left, int right)
{
Stack s;
StackInit(&s);
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 = PartSort2(arr, left, right);
// 计算左区间内容 [left, div)
StackPush(&s, left);
StackPush(&s, div);
// 计算右区间 [div + 1, right]
StackPush(&s, div + 1);
StackPush(&s, right);
}
}
StackDestroy(&s);
}
4. 影响快排效率的因素
如果该序列有序,并且每次取值都取的使序列中最大值作为基准值,则递归时的二叉树结构会退化为单只树,性能会变得极差。时间复杂度变为
O(n^2)
。但是可以优化它的取基准值的方式,来进行优化。我们采用的方式是三值取中法(取出序列中 头 中 尾 三个元素,取值在中间的那个作为基准)。优化后的平均时间复杂度为O(n*log n)
四. 归并排序
1. 递归实现
实现原理
归并排序是建立在归并操作的一种有效的排序算法,采用分治思想。
基本过程是先分再归,将数组不断平均划分,直到只剩下一个元素,再根据合并两个有效数组的方式对分开的序列进行合并。直到整个数组合并完成。
先递归再归并。
图解
时间空间复杂度
时间复杂度:
O(n* log n)
每一层都是O(n)
递归深度为O(log n)
空间复杂度:O(n)
稳定性 稳定
代码实现
// 思路与合并两个有序数组相同
void MergeArr(int* arr, int left, int mid, int right, int* res)
{
int begin1 = left;
int end1 = mid;
int begin2 = mid;
int end2 = right;
int index = left;
// 进行归并排序,谁小就把谁放到res数组里
while (begin1 < end1 && begin2 < end2)
{
if (arr[begin1] < arr[begin2])
{
res[index++] = arr[begin1++超详细总结基于比较的七大经典 排序 -- 不会的童鞋快进来补习