目录
一. 直接插入排序
二:选择排序
三:冒泡排序
四.堆排序
五:希尔排序
六:快速排序(递归与非递归)
七.归并排序(递归与非递归)
一. 直接插入排序
🌟排序思路
直接插入排序的基本原理是将一条记录插入到已排好的有序表中,从而得到一个新的、记录数量增1的有序表,其思路就和我们摸扑克牌一样,每摸到一张牌按照大小把他插入到对应位置,这样等摸完全部的牌时,我们手里的牌就是有序的
⛲动态图解:
💬特点
🚩时间复杂度:
O(N^2)(若待排序表为有序的则时间复杂度为O(N))
🚩空间复杂度:
空间复杂度为O(1)
🚩稳定性:
稳定
⚡代码演示:
void InsertSort(int* a, int n)
{
for (int i = 0; i < n - 1; i++)
{
int end = i;
//首先保存一下待插入的元素
int tmp = a[end + 1];
//比较的最坏情况时end=0
while (end >= 0)
{
//若待排序元素比a[end]小,则让a[end]向后移动腾位置
if (tmp < a[end])
{
a[end + 1] = a[end];
end--;
}
else
{
//由于本来数组就为有序数组,当不满足上述条件时,说明找到了待插入的位置
break;
}
}
//插入数据
a[end + 1] = tmp;
}
}
二:选择排序
🌟排序思路
每次选出数组的最小值和最大值,分别置于数组头尾处,缩小排序范围重复操作
⛲动态图解:
💬特点
🚩时间复杂度:
时间复杂度为:O(N^2)
🚩空间复杂度:
空间复杂度为O(1)
🚩稳定性:
不稳定
⚡代码演示:
void SelectSort(int* a, int n)
{
int begin = 0, end = n - 1;
while (begin < end)
{
int mini = begin, maxi = begin;
for (int i = begin + 1; i <= end; ++i)
{
if (a[i] < a[mini])
{
mini = i;
}
if (a[i] > a[maxi])
{
maxi = i;
}
}
Swap(&a[begin], &a[mini]);
if (maxi == begin)
{
maxi = mini;
}
Swap(&a[end], &a[maxi]);
++begin;
--end;
}
}
三:冒泡排序
🌟排序思路
比较相邻元素的大小,将大的元素往后交换,每一轮比较能确定一个最大值的位置
⛲动态图解:
💬特点
🚩时间复杂度:
时间复杂度为:O(N^2)
🚩空间复杂度:
空间复杂度为O(1)
🚩稳定性:
稳定
⚡代码演示:
/* 冒泡排序 */
void BubbleSort(int arr[], int length)
{
for (int i = 0; i < length; i++)
{
for (int j = 0; j < length - i - 1; j++)
{
if (arr[j] > arr[j + 1])
{
int temp;
temp = arr[j + 1];
arr[j + 1] = arr[j];
arr[j] = temp;
}
}
}
}
四.堆排序
🌟排序思路
1.首先将待排序数组建为大堆,此时堆顶元素就为数组最大值了
2.交换堆顶和堆尾元素,此时最大元素就到了堆尾,目前数组最大元素就排好了,现在就假设堆里没有当前这个最大元素了,堆头下面的左右子树仍然是大堆,只需要再将堆顶元素向下调整到合适位置,剩下的n-1个元素还是大堆
3.堆头堆尾交换,向下调整,如此反复就可排序
ps.排序以升序为例,升序建大堆,降序建小堆
💬特点
🚩时间复杂度:
O(N*lgN)
🚩空间复杂度:
O(1)
🚩稳定性:
不稳定
⚡代码演示:
#include<stdio.h>
typedef int HeapDataType;
void swap(HeapDataType* a, HeapDataType* b)
{
int tmp = *a;
*a = *b;
*b = tmp;
}
//大堆
void AdjustUp(HeapDataType* a, int child)
{
int parent = (child - 1) / 2;
while (child>0)
{
if (a[parent] < a[child])
{
swap(&a[parent], &a[child]);
child = parent;
parent = (parent - 1) / 2;
}
else
{
break;
}
}
}
//大堆
void AdjustDown(HeapDataType* a, int size, int parent)
{
int child = parent * 2 + 1;
while (child<size)
{
//找较大的孩子
if (a[child + 1] > a[child] && child+1<size)
{
child = child + 1;
}
if (a[parent] < a[child])
{
swap(&a[parent], &a[child]);
parent = child;
child = parent * 2 + 1;
}
else
{
break;
}
}
}
void HeapSort(HeapDataType* a, int size)
{
//建堆
for (int i = 1; i < size; i++)
{
AdjustUp(a, i);
}
//排序:升序
int end = size - 1;
while (end>0)
{
swap(&a[0], &a[end]);
AdjustDown(a, end, 0); //end指最后一个元素,同时end的值为前面元素的个数
end--;
}
}
五:希尔排序
🌟排序思路
- 预排序:先将数组分组,将每个组排序,目的是让整个数组相对有序
- 插入排序:待数组相对有序后,进行直接插入排序,这样效率比较高
⛲图解:
假设待排序数组为[9 8 7 6 5 4 3 2 1]
1.我们将他们每隔三个坐标分为一组,假设gap=3
2.对三个数组分别进行直接插入排序,使数组整体相对有序
3.对数组整体进行插入排序
💬特点
🚩时间复杂度:
O(N^1.3)
🚩空间复杂度:
O(1)
🚩稳定性:
不稳定
⚡代码演示:
void ShellSort(int* a, int n)
{
//首先将gap定为n
int gap = n;
/*while (gap > 1)
{
gap = gap / 3 + 1;
for (int j = 0; j < gap; j++)
{
for (int i = j; i < n - gap; i += gap)
{
int end = i;
int tmp = a[end + gap];
while (end >= 0)
{
if (tmp < a[end])
{
a[end + gap] = a[end];
end -= gap;
}
else
{
break;
}
}
a[end + gap] = tmp;
}
}
}*/
while (gap > 1)
{
gap = gap / 3 + 1;
for (int i = 0; i < n - gap; i++)
{
int end = i;
int tmp = a[end + gap];
while (end >= 0)
{
if (tmp < a[end])
{
a[end + gap] = a[end];
end -= gap;
}
else
{
break;
}
}
a[end + gap] = tmp;
}
}
}
六:快速排序(递归与非递归)
1.⚡hoare
🌟排序思路
将数组第一个元素定位关键值,定义begin和end指针,先让end从后往前找到比关键值小的数,begin从前往后找比关键值大的数,然后交换两数,直到 begin==end,再让关键值和begin所指的元素交换,最后返回关键值所在位置,便于后续进行递归或非递归操作
⛲动态图解:
⚡代码演示:
void swap(int* a, int* b)
{
int tmp = *a;
*a = *b;
*b = tmp;
}
//hoare
int PartSort1(int* a, int begin, int end)
{
int left = begin, right = end;
int keyi = begin;
while (left < right)
{
//右边找小
while (left < right && a[right] >= a[keyi])
{
right--;
}
//左边找大
while (left < right && a[left] < a[keyi])
{
left++;
}
swap(&a[left], &a[right]);
}
swap(&a[left], &a[keyi]);
return left;
}
2.⚡挖坑法
🌟排序思路
首先将关键值定为数组第一个元素,并将坑位定为begin,先让end从后往前找到比关键值小的数,将这个数放到坑位,并更新坑位,再让begin从前往后找比关键值大的数,将这个数放到坑位,并更新坑位,直到 begin==end,再让关键值和坑位的元素交换,最后返回关键值所在位置
⛲动态图解:
⚡代码演示:
//挖坑法
int PartSort2(int* a, int begin, int end)
{
int mid = GetMid(a, begin, end);
swap(&a[begin], &a[mid]);
int key = a[begin];
int hole = begin;
while (begin < end)
{
//右边找小,填入坑内,更新坑位
while (begin<end && a[end]>=key)
{
--end;
}
a[hole] = a[end];
hole = end;
//左边找大,填入坑内,更新坑位
while (begin<end && a[begin]<=key)
{
++begin;
}
a[hole] = a[begin];
hole = begin;
}
a[hole] = key;
return hole;
}
3.⚡双指针法
🌟排序思路
将数组第一个元素定为关键值,定义两个指针prev和cur,先让prev指向数组的第一个元素,cur指向prev的下一个元素,cur的作用是找比关键值小的元素,若cur所指元素不小于关键值则cur++,直到cur所值元素小于关键值,此时,prev和cur之间的元素都是大于关键值的元素,若prev+1不是cur的话就可以让prev++所指元素与cur所指元素交换了,直到cur指向数组的最后一个元素
⛲动态图解:
⚡代码演示:
//双指针法
int PartSort3(int* a, int begin, int end)
{
int mid = GetMid(a, begin, end);
swap(&a[begin], &a[mid]);
int key = begin;
int prev = begin;
int cur = prev + 1;
while (cur <= end)
{
if (a[cur] < a[key] && ++prev != cur)
{
swap(&a[prev], &a[cur]);
}
cur++;
}
swap(&a[prev], &a[key]);
return prev;
}
💬特点
🚩时间复杂度:
最好情况:O(n*lgn)
最坏情况:O(n^2)
🚩空间复杂度
O(lgn)
🚩稳定性
不稳定
🚩快速排序递归实现
⛲小优化:
上述三个方法都是快速排序的单趟排序,但是上述排序还有一个小缺陷,因为三个方法都是固定第一个元素为关键值的,如果数组为有序的,那么从后往前找小就要遍历整个数组,效率会很小,所以通常会再写一个找中间值的函数:在数组开头结尾和中间三个数中找出一个大小在中间的数,并让这个数和数组第一个数交换,这样就会减少上述情况的发生
int GetMid(int* a, int begin, int end)
{
int mid = (begin + end) / 2;
if (a[begin] > a[mid])
{
if (a[mid] > a[end])
return mid;
else if (a[end] > a[begin])
return end;
else
return begin;
}
else
{
if (a[begin] > a[end])
return begin;
else if (a[end] > a[mid])
return mid;
else
return end;
}
}
void swap(int* a, int* b)
{
int tmp = *a;
*a = *b;
*b = tmp;
}
//hoare
int PartSort1(int* a, int begin, int end)
{
int mid = GetMid(a, begin, end);
swap(&a[begin], &a[mid]);
int left = begin, right = end;
int keyi = begin;
while (left < right)
{
//右边找小
while (left < right && a[right] >= a[keyi])
{
right--;
}
//左边找大
while (left < right && a[left] < a[keyi])
{
left++;
}
swap(&a[left], &a[right]);
}
swap(&a[left], &a[keyi]);
return left;
}
//挖坑法
int PartSort2(int* a, int begin, int end)
{
int mid = GetMid(a, begin, end);
swap(&a[begin], &a[mid]);
int key = a[begin];
int hole = begin;
while (begin < end)
{
//右边找小,填入坑内,更新坑位
while (begin<end && a[end]>=key)
{
--end;
}
a[hole] = a[end];
hole = end;
//左边找大,填入坑内,更新坑位
while (begin<end && a[begin]<=key)
{
++begin;
}
a[hole] = a[begin];
hole = begin;
}
a[hole] = key;
return hole;
}
//双指针法
int PartSort3(int* a, int begin, int end)
{
int mid = GetMid(a, begin, end);
swap(&a[begin], &a[mid]);
int key = begin;
int prev = begin;
int cur = prev + 1;
while (cur <= end)
{
if (a[cur] < a[key] && ++prev != cur)
{
swap(&a[prev], &a[cur]);
}
cur++;
}
swap(&a[prev], &a[key]);
return prev;
}
void QuickSort(int* a, int begin,int end)
{
if (begin >= end)
return;
//三种方法任选其一即可
int keyi = PartSort3(a, begin, end);
QuickSort(a, begin, keyi - 1);
QuickSort(a, keyi + 1, end);
}
🚩快速排序非递归实现
🌟实现思路:
1.创建一个栈,将数组的右边界下标和左边界下标依次入栈
2.循环弹出数组的左右边界下标,并对该区间进行单趟排序,确定关键值的下标,分为左右两个区间
3.若左区间元素个数大于一个,将左区间右边界下标和左边界下标依次入栈,右区间同理
4.重复操作步骤2 3直到栈为空
⚡代码演示:
void QuickSortNonR(int* a, int begin, int end)
{
ST s;
STInit(&s);
STPush(&s,end);
STPush(&s,begin);
while (!STEmpty(&s))
{
int left = STTop(&s);
STPop(&s);
int right = STTop(&s);
STPop(&s);
int key = PartSort1(a, left, right);
if (left < key - 1)
{
STPush(&s, key - 1);
STPush(&s, left);
}
if (right > key + 1)
{
STPush(&s, right);
STPush(&s, key+1);
}
}
STDestroy(&s);
}
七.归并排序(递归与非递归)
一:⛲递归实现
🌟算法思路
归并排序是用分治思想,分治模式在每一层递归上有三个步骤:
分解(Divide): 将n个元素分成个含n/2个元素的子序列。
解决(Conquer):用合并排序法对两个子序列递归的排序。
合并(Combine):合并两个已排序的子序列已得到排序结果。
该算法需要先将数组分解,直到每个子序列为一个元素,再将子序列两两合并排序,思路可以参考二叉树的后序递归
💬特点
平均时间复杂度:O(nlogn)
最佳时间复杂度:O(n)
最差时间复杂度:O(nlogn)
空间复杂度:O(n)
⚡动图展示:
⚡代码演示:
void _MergeSort(int* a, int begin, int end, int* tmp)
{
if (begin >= end)
return;
//分解
int mid = (begin + end) / 2;
_MergeSort(a, begin, mid, tmp);
_MergeSort(a, mid+1, end, tmp);
// 归并
int begin1 = begin, end1 = mid;
int begin2 = mid + 1, end2 = end;
int i = begin;
while (begin1 <= end1 && begin2 <= end2)
{
if (a[begin1] < a[begin2])
{
tmp[i++] = a[begin1++];
}
else
{
tmp[i++] = a[begin2++];
}
}
while(begin1 <= end1)
{
tmp[i++] = a[begin1++];
}
while (begin2 <= end2)
{
tmp[i++] = a[begin2++];
}
memcpy(a + begin, tmp + begin, sizeof(int) * (end - begin + 1));
}
void MergeSort(int* a, int n)
{
int* tmp = (int*)malloc(sizeof(int) * n);
if (tmp == NULL)
{
perror("malloc fail");
return;
}
_MergeSort(a, 0, n - 1, tmp);
free(tmp);
}
二:⛲非递归实现
🌟算法思路:
归并排序的递归实现是先将数组分解,直到每个子序列只有一个元素,在将子序列两两归并,非递归的实现思路则没有了分解的过程,直接将数组元素一个与一个归并,在两个与两个归并,在四个与四个归并.......,直到最后两组归并,数组变为有序数组
⚡代码演示:
void MergeSortNonR(int* a, int begin, int end)
{
int n = end - begin + 1;
int* tmp = (int*)malloc(sizeof(int) * (n));
if (tmp == NULL)
{
perror("malloc");
return;
}
int gap = 1;
//当gap小于n时两个组才都有元素,才需要归并
while (gap < n)
{
int j = 0;
for (size_t i = 0; i < n; i += 2 * gap)
{
int begin1 = i, end1 = i + gap - 1;
int begin2 = i + gap, end2 = i + 2 * gap - 1;
//边界管理
if (end1 >= n||begin2>=n)
{
break;
}
if (end2 >= n)
{
end2 = n - 1;
}
while (begin1 <= end1 && begin2 <= end2)
{
if (a[begin1] <= a[begin2])
{
tmp[j++] = a[begin1++];
}
else
{
tmp[j++] = a[begin2++];
}
}
while (begin1 <= end1)
{
tmp[j++] = a[begin1++];
}
while (begin2 <= end2)
{
tmp[j++] = a[begin2++];
}
//归并完后拷贝给原数组
memcpy(a + i, tmp + i, sizeof(int) * (end2 - i + 1));
}
gap *= 2;
}
free(tmp);
}