目录
插入排序
基本思想
可以将一组混乱无序的数字看作两个类别,一个是有序的序列,一个是无序的序列。排序开始前,有序的序列为该数组的第一个,其余的数字均属于无序的序列。接下来插入排序的核心思想就是:将无序序列中的数字,逐个拿出,插入有序序列中,直至无序序列没有数字,即为排序完成。
时间复杂度为:
程序实现(InsertSort)
void InsertSort(int* arr, int len)
{
int i = 0;
while (i < len - 1)
{
//i表示的是有序序列中数字的个数
int end = i;
int tmp = arr[end + 1];
while (end >= 0)
{
//[0,end]为有序序列,[end + 1,len - 1]为无序序列,将arr[end + 1]插入,使得[0,end + 1]有序
if (arr[end] > tmp)
{
//将arr[end]向后移动一个位置
arr[end + 1] = arr[end];
end--;
}
if (arr[end] <= tmp)
{
arr[end + 1] = tmp;
break;
}
}
i++;
}
}
希尔排序
基本思路
希尔排序是插入排序的优化,希尔排序的基本思路为:
1.先进行预排序,使数组接近于有序
2.再进行插入排序
gap 的作用为:进行多次预排序,使数组尽可能趋于有序。gap越大,数字可以移动的幅度越大;gap越小,则说明数组趋于有序,当gap==1,即为插入排序。
时间复杂度: ~
程序实现(ShellSort)
//希尔排序是插入排序的优化
//1.先进行预排序,使数组接近于有序
//2.再进行插入排序
void ShellSort(int* arr, int len)
{
int gap = len;
//gap == 1 插入排序
//gap > 1 预排序
while (gap > 1)
{
gap = gap / 3 + 1;
int i = 0;
//间隔为gap的多趟数据同时排序
for (i = 0; i < len - gap; i++)
{
int end = i;
int tmp = arr[end + gap];
while (end >= 0)
{
if (arr[end] >= tmp)
{
arr[end + gap] = arr[end];
end -= gap;
}
if (arr[end] <= tmp)
{
arr[end + end] = tmp;
break;
}
}
}
}
}
堆排序
基本思路
堆的逻辑结构是一棵完全二叉树,物理结构是一个数组。我们可以通过计算数组下标从而判断其父子关系。
parent * 2 + 1 = leftchild
parent * 2 + 2 = rightchild
parent = (child - 1) / 2
堆可以分为两类,最大堆和最小堆。
最大堆 (大堆) | 最大堆中所有的父亲都大于等于孩子 | 根节点储存的值是最大值 |
最小堆 (小堆) | 最小堆中所有的父亲都小于等于孩子 | 根节点储存的值是最大值 |
接下来,我们以数组arr[] = {1,18,5,9,3,8,12,2}为例,进行分析。
如图,堆的物理结构是arr这个数组,其逻辑结构如图。
我们堆排序的前提是建堆(时间复杂度:),这里我们要将数组arr变成一个小堆。这里我们用到的算法是向下调整算法。
向下调整算法:前提是所给的根节点 root 的左右子树都是小堆,利用这个算法可以将 root 为根节点的二叉树转换为一个小堆。具体思路如图。
如图,根节点的左子树和右子树都是小堆,符合向下调整算法的前提。
基本步骤为:
接下来,我们对于数组arr[] = {1,18,5,9,3,8,12,2},它的根节点的左右子树并不满足是小堆的前提。但是我们可以知道,当一棵二叉树深度为2时,这个二叉树的左右子树必然是小堆(以为根节点下就是叶子节点,叶子节点一定是小堆)。所以我们可以从倒数一个非叶子节点的子树开始向下调整,这样子自下而上,就可以保证整个二叉树都变成小堆了。如图:
堆排序
升序 | 建大堆,找最大值 |
降序 | 建小堆,找最小值 |
还是以数组arr[] = {1,18,5,9,3,8,12,2}为例,我们需要升序,所以应该建大堆。
由于最大堆的特性,根节点的值是数组中的最大值,所以我们可以利用这个特性,进行以下操作。
时间复杂度:
程序实现(HeapSort)
void AdjustDown(int* arr, int len, int root)
{
//向下调整算法
//前提:左右子树均为大堆
//取 min(leftchild, rightchild),和 parent 比较
//如果 parent < min(leftchild, rightchild), 则交换位置
int parent = root;
//左孩子的下标
int child = parent * 2 + 1;
//左孩子和右孩子比大小
while (child < len)
{
if (arr[child] < arr[child + 1] && child + 1 < len)
{
child += 1;
}
if (arr[parent] < arr[child])
{
Swap(&arr[parent], &arr[child]);
}
//因为左右子树是大堆,父节点比左右子树的根节点还要大,所以父节点一定是最大的
else
{
break;
}
//迭代条件
parent = child;
child = parent * 2 + 1;
}
}
void HeapSort(int* arr, int len)
{
//建堆
//向下调整算法的前提是左右子树均为大堆
//所以我们可以从倒数一个非叶子节点的子树开始向下调整
//因为叶子节点没有孩子节点,所以一定满足左右子树是大堆
int i;
for (i = (len - 2) / 2; i >= 0; i--)
{
AdjustDown(arr, len, i);
}
int end = len - 1;
for (end = len - 1; end > 0; end--)
{
Swap(&arr[end], &arr[0]);
AdjustDown(arr, end, 0);
}
}
选择排序
基本思路
每一次从待排序的数据元素中选出最小(或最大)的一个元素,存放在序列的起始位置,直到全
部待排序的数据元素排完 。
如下动图所示
时间复杂度:
程序实现(SelectSort)
void SelectSort(int* arr, int len)
{
int i = 0;
for (i = 0; i < len; i++)
{
int end = i;
int min = end;
for (end = i; end < len; end++)
{
if (arr[end] < arr[min])
{
min = end;
}
}
Swap(&arr[i], &arr[min]);
}
}
冒泡排序
基本思路
将键值较大的记录向序列的尾部移动,或者将键值较小的记录向序列的前部移动。
如下图所示
时间复杂度:
程序实现(BubbleSort)
void BubbleSort(int* arr, int len)
{
int i = 0;
int end = len - 1;
for (end = len - 1; end > 0; end--)
{
int flag = 0;
for (i = 1; i <= end; i++)
{
if (arr[i - 1] > arr[i])
{
Swap(&arr[i - 1], &arr[i]);
flag = 1;
}
}
//如果某一趟并未进行一次交换,则说明该趟已经有序,不需要继续循环
if (flag == 0)
{
break;
}
}
快速排序
基本思路
流程如图所示
一.选取基准值
一般来说,我们选取数组中第一个为基准值。但是,如果要排序的数组是有序的,时间复杂度为 ,我们可以采取三数取中的方法避免该情况:,即取数组中第一个数,最后一个数,和中间一个数的中间大小值作为基准值。
//优化:三数取中
int GetMidIndex(int* arr, int left, int right)
{
int mid = (left + right) / 2;
if (arr[left] > arr[right])
{
if (arr[mid] > arr[left])
{
return left;
}
else if (arr[mid] < arr[right])
{
return right;
}
else
{
return mid;
}
}
else
{
if (arr[mid] < arr[left])
{
return left;
}
else if (arr[mid] > arr[right])
{
return right;
}
else
{
return mid;
}
}
}
二.单趟排序
1.挖坑法
任取待排序元素序列中的某元素作为基准值,按照该排序码将待排序集合分割成两子序列,左子序列中所有元素均小于基准值,右子序列中所有元素均大于基准值,然后最左右子序列重复该过程,直到所有元素都排列在相应位置上为止。
如图所示,先进行第一趟的快速排序。
此时我们可以发现,基准值6将原序列分为两个部分,左子列的所有的数都小于基准值,右子列的所有数都大于基准值,且基准值已经放在了正确的位置。
int PartSort1(int* arr, int left, int right)
{
int index = GetMidIndex(arr, left, right);
Swap(&arr[left], &arr[index]);
//将arr[begin]放置在正确的位置
int begin = left;
int end = right;
int key = arr[begin];
int pivot = begin;
while (begin < end)
{
//end向前遍历,找小于基准数的值
while (arr[end] >= key && begin < end)
{
end--;
}
arr[begin] = arr[end];
pivot = end;
//begin向后遍历,找大于基准数的值
while (arr[begin] <= key && begin < end)
{
begin++;
}
arr[end] = arr[begin];
pivot = begin;
}
arr[end] = key;
return pivot;
}
2.左右指针法
如下图所示,还是以第一个数确立为基准值,begin,end指针同时移动:begin指向的值大于基准值时,停止;end指向的值小于基准值时,停止。交换arr[begin],arr[end]。直至begin,end相遇,停止循环,将此时二者指向的值和基准值(数组第一个数)交换位置。
int PartSort2(int* arr, int left, int right)
{
int index = GetMidIndex(arr, left, right);
Swap(&arr[left], &arr[index]);
//将arr[begin]放置在正确的位置
int begin = left;
int end = right;
int pivot = begin;
while (begin < end)
{
while (arr[end] >= arr[pivot] && begin < end)
{
end--;
}
while (arr[begin] <= arr[pivot] && begin < end)
{
begin++;
}
Swap(&arr[begin], &arr[end]);
}
Swap(&arr[begin], &arr[pivot]);
pivot = begin;
return pivot;
}
3.前后指针法(快慢指针法)
cur向后遍历,每当arr[cur]小于基准值时,先将prev向后遍历一位,arr[cur]和arr[prev]交换位置。(注意先后顺序)
int PartSort3(int* arr, int left, int right)
{
int index = GetMidIndex(arr, left, right);
Swap(&arr[left], &arr[index]);
int cur = left + 1;
int prev = left;
int pivot = left;
while (cur <= right)
{
if (arr[cur] < arr[pivot])
{
prev++;
Swap(&arr[cur], &arr[prev]);
}
cur++;
}
Swap(&arr[pivot], &arr[prev]);
pivot = prev;
return pivot;
}
三.分治递归
接下来,我们只需要将[ 0, pivot - 1]和[ pivot + 1, len - 1]这两个子序列排序即可。
分治递归:只要左边界等于于右边界,则说明子序列中只有一个数,则排序完成(递归终止条件)
时间复杂度:
时间复杂度:(有序的情况下时间最长)
小区间优化:当数组被分割到一个比较小的数量时,可以采取直接插入排序,减少递归调用的次数,以缩短运行时间。
if (pivot - left < 10)
{
InsertSort(arr + left, pivot - left);
}
else
{
_QuickSort(arr, left, pivot - 1);
}
if (right - pivot < 10)
{
InsertSort(arr + pivot + 1, right - pivot);
}
else
{
_QuickSort(arr, pivot + 1, right);
}
程序实现(QuickSort)
void QuickSort(int* arr, int len)
{
_QuickSort(arr, 0, len - 1);
}
void _QuickSort(int* arr, int left, int right)
{
if (right <= left)
{
return;
}
//int pivot = PartSort1(arr, left, right);
//int pivot = PartSort2(arr, left, right);
int pivot = PartSort3(arr, left, right);
//_QuickSort(arr, left, pivot - 1);
//_QuickSort(arr, pivot + 1, right);
//小区间优化
if (pivot - left < 10)
{
InsertSort(arr + left, pivot - left);
}
else
{
_QuickSort(arr, left, pivot - 1);
}
if (right - pivot < 10)
{
InsertSort(arr + pivot + 1, right - pivot);
}
else
{
_QuickSort(arr, pivot + 1, right);
}
}
//优化:三数取中
int GetMidIndex(int* arr, int left, int right)
{
int mid = (left + right) / 2;
if (arr[left] > arr[right])
{
if (arr[mid] > arr[left])
{
return left;
}
else if (arr[mid] < arr[right])
{
return right;
}
else
{
return mid;
}
}
else
{
if (arr[mid] < arr[left])
{
return left;
}
else if (arr[mid] > arr[right])
{
return right;
}
else
{
return mid;
}
}
}
int PartSort1(int* arr, int left, int right)
{
int index = GetMidIndex(arr, left, right);
Swap(&arr[left], &arr[index]);
//将arr[begin]放置在正确的位置
int begin = left;
int end = right;
int key = arr[begin];
int pivot = begin;
while (begin < end)
{
//end向前遍历,找小于基准数的值
while (arr[end] >= key && begin < end)
{
end--;
}
arr[begin] = arr[end];
pivot = end;
//begin向后遍历,找大于基准数的值
while (arr[begin] <= key && begin < end)
{
begin++;
}
arr[end] = arr[begin];
pivot = begin;
}
arr[end] = key;
return pivot;
}
int PartSort2(int* arr, int left, int right)
{
int index = GetMidIndex(arr, left, right);
Swap(&arr[left], &arr[index]);
//将arr[begin]放置在正确的位置
int begin = left;
int end = right;
int pivot = begin;
while (begin < end)
{
while (arr[end] >= arr[pivot] && begin < end)
{
end--;
}
while (arr[begin] <= arr[pivot] && begin < end)
{
begin++;
}
Swap(&arr[begin], &arr[end]);
}
Swap(&arr[begin], &arr[pivot]);
pivot = begin;
return pivot;
}
int PartSort3(int* arr, int left, int right)
{
int index = GetMidIndex(arr, left, right);
Swap(&arr[left], &arr[index]);
int cur = left + 1;
int prev = left;
int pivot = left;
while (cur <= right)
{
if (arr[cur] < arr[pivot])
{
prev++;
Swap(&arr[cur], &arr[prev]);
}
cur++;
}
Swap(&arr[pivot], &arr[prev]);
pivot = prev;
return pivot;
}
非递归实现快速排序
递归存在一定的缺陷,我们以下面的程序为例
我们写一个函数 f(n),用于计算小于n的所有正整数的和。该函数就是使用了递归算法。
int f(int n)
{
return n <= 1 ? 1 : f(n - 1) + 1;
}
int main()
{
printf("%d\n", f(10000));
return 0;
}
但是当我们计算f(10000)时,无法得出结果。
我们可以画出递归图理解:
所以,递归有一个致命缺陷,就是如果递归深度太深,可能会导致栈空间不够用,从而导致栈溢出。
在这里,我们将会借助数据结构的栈模拟递归过程,以增加可调用的空间。
void _QuickSort(int* arr, int left, int right)
{
if (right <= left)
{
return;
}
int pivot = PartSort1(arr, left, right);
_QuickSort(arr, left, pivot - 1);
_QuickSort(arr, pivot + 1, right);
}
这是未进行三数取中优化的快速排序,接下来我们将会通过画出递归展开图的方式,理解快速排序的逻辑,并通过数据结构的栈模拟递归调用的过程。
程序实现:
我们将左区间和右区间放入一个结构体 r 中,而栈st存储该结构体。
struct range
{
int left;
int right;
};
- 栈中存储的是无序的序列。
- 我们每次取出栈顶的元素,进行单趟排序,即可将原来一个无序序列 [r.left, r.right] 分为 [r.left, pivot - 1] 和 [pivot + 1, r.right]两个无序序列
- 如果新的无序序列只有一个值,则已经有序,不需要压入栈中;
- 如果新的无序序列还有多个值,则说明无序,需要重新压入栈中。
- 当栈中没有元素时,即完成排序操作。
//非递归实现快排:借助数据结构的栈模拟递归过程
void QuickSortNonR(int* arr, int len)
{
ST st;
StackInit(&st);
struct range r;
r.left = 0;
r.right = len - 1;
StackPush(&st, r);
//如果栈不为空,则说明需要排序
while (!StackEmpty(&st))
{
//取栈顶元素
r = StackTop(&st);
StackPop(&st);
//单趟排序
//将一个无序序列 [r.left, r.right] 分为 [r.left, pivot - 1] 和 [pivot + 1, r.right]两个无序序列
int pivot = PartSort1(arr, r.left, r.right);
struct range tmp;
tmp.left = 0;
tmp.right = 0;
if (pivot + 1 < r.right)
{
tmp.left = pivot + 1;
tmp.right = r.right;
StackPush(&st, tmp);
}
if (r.left < pivot - 1)
{
tmp.left = r.left;
tmp.right = pivot - 1;
StackPush(&st, tmp);
}
}
StackDestory(&st);
}
归并排序
基本思路
如图所示,将原序列依次二分,分到只有一个数时,进行层层有序合并。
单趟排序:
前提:将两个有序的数组,合并成一个有序的数组。
具体步骤:当两个区间有序,同时遍历这两个序列,取二者中较小的值插入临时的数组tmp中,如果这两个序列有剩余,则将剩余的值直接插入tmp中。然后将tmp中的值复制给原数组。
详见:10.30链表进阶_zhangyuaizhuzhu的博客-CSDN博客
//归并:两个有序序列归并到一个有序序列中
int cur = left;
int prev = mid + 1;
int i = left;
while (cur <= mid && prev <= right)
{
if (arr[cur] < arr[prev])
{
tmp[i++] = arr[cur++];
}
else
{
tmp[i++] = arr[prev++];
}
}
while (cur <= mid)
{
tmp[i++] = arr[cur++];
}
while (prev <= right)
{
tmp[i++] = arr[prev++];
}
时间复杂度:
空间复杂度:
程序实现(MergeSort)
以测试数组arr[] = { 10,6,7,1,3,9,4,2 }为例:
进行归并排序时,由递归展开图可知:
该程序的思路类似于二叉树的后序遍历,后序遍历是对二叉树的叶子节点进行打印输出;而归并排序也类似,每次遍历到最小的区间,再进行排序操作。