文章目录
1.直接插入排序
直接插入排序是较为简单直观的排序算法,在数据内选择合适的位置,将数据插入进去。通过构建一个有序数列,之后找到合适位置进行插入。步骤如下(以升序为例)
1.先完成单趟排序的代码,从第一个位置开始,所有元素被认为已经排序。
2.将下一个位置的元素保存在tmp里,之后从后往前开始遍历。
3.如果下一个元素大于tmp,则将tmp插入到这个元素的后面,该元素移动到后一位。
4.重复步骤2、4。
5.继续下一位循环,直到整个序列结束。
动画演示:
图片演示:
代码实现:
//直接插入排序
void InsertSort(int* a, int n)
{
assert(a);
for (int i = 0; i < n - 1; i++)
{
//单趟排序
int end = i;//每一次将end向前走一步
int tmp = a[end + 1];
while (end >= 0)
{
//比插入的数大就后移
if (tmp < a[end])
{
a[end + 1] = a[end];
end--;
}
else
{
break;
}
}
a[end + 1] = tmp;
//代码执行到此有两种情况
//1.待插入的元素已经找到适合插入的位置,已经插入
//2.待插入的元素比当前所有的数都要小,跳出了while循环
}
}
直接插入排序特点总结:
1.时间复杂度为O(N~N^2)
2.空间复杂度O(1)
3.稳定性:稳定
4.元素越接近有序,排序效率越快;反之,逆序时效率最慢
2.希尔排序(插入排序的优化)
希尔排序是对直接插入排序的优化,插入排序的特点是越有序效率越快,因此通过预排序多次分组将序列变得接近有序,这样可以优化代码效率。
步骤如下:
1.选取一个小于N的gap做为增量,将所有距离为gap的数据进行直接插入排序,然后取第二个gap做第二组的增量,重复如上步骤。
2.取gap为1时,增量为1,此时进行插入排序,既可以保证完成整段序列的排序,同时因为之前的排序,序列已经趋近有序,可以增加效率。
动画演示:
图示:
代码实现 :
//希尔排序,对直接插入排序的优化,时间复杂度在O(N^1.3-N^2)
void ShellSort(int* a, int n)
{
assert(a);
//1.gap>1相当于预排序,让数组变得接近有序
//2.gap==1相当于直接插入排序,保证有序
int gap = n;
while (gap > 1)
{
gap = gap / 3 + 1;//保证最后一次gap是1
//gap == 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;
}
PrintArray(a, n);
}
}
希尔排序的特点总结:
1.平均时间复杂度O(N*logN)
2.空间复杂度O(1)
3.稳定性:不稳定
3.选择排序
选择排序的大体思路是遍历序列选出最小值和最大值,将最小值放置在前面,最大值放在后面,直到遍历整个序列,完成升序的排序。选择排序简单直观,无论什么数据放进去都是一样的时间复杂度,因此数据规模越小越好。
步骤如下:
1.保存开头和结尾的位置,将最大值max和min同时放在第一位,准备寻找最大值和最小值。
2.遍历序列,max和min同时出发寻找,如果遇到比max大的数,则将其赋值给max,如果遇到比min小的数则将其赋值给min,直到序列遍历完。
3.确定最大值和最小值后,将其与序列开头begin和结尾end的数进行交换,再++begin和end,直到begin>end
动画演示:
代码实现:
void Swap(int* p1, int* p2)
{
int tmp = *p1;
*p1 = *p2;
*p2 = tmp;
}
//选择排序
void SelectSort(int* a, int n)
{
assert(a);
int begin = 0;
int end = n - 1;
while (begin < end)
{
int maxi,mini;
maxi = mini = begin;
//找到最大和最小值
for (int i = begin+1; i <= end; i++)
{
if (a[i] > a[maxi])
{
maxi = i;
}
if (a[i] < a[mini])
{
mini = i;
}
}
Swap(&a[begin], &a[mini]);
//如果maxi和begin位置重合,则maxi的位置需要修正
if (begin == maxi)
{
maxi = mini;
}
Swap(&a[end], &a[maxi]);
begin++;
end--;
}
}
选择排序特点总结:
1.时间复杂度为O(N^2)
2.空间复杂度为O(N^2)
3.稳定性:不稳定
4.无论原序列是不是有序,时间复杂度都是O(N^2),都要重新选择排序。
4.堆排序
堆排序的主要思想是建堆,升序建大堆,降序则建小堆,这样能保证第一位是最大值或最小值,便于交换。
步骤如下:
1.构建一个向下调整算法,构建出大堆。
2.交换第一位和最后一位end,交换后再进行向下调整,选出最大值,end--,再进行交换,直到遍历整段序列。
动画演示:
代码实现:
//堆排序
//时间复杂度是O(N*logN)
void HeapSort(int* a, int n)
{
//升序,建大堆
for (int i = (n - 1 - 1) / 2; i >= 0; i--)
{
AdjustDown(a, n, i);
}
int end = n - 1;
while (end > 0)
{
Swap(&a[0], &a[end]);
AdjustDown(a, end, 0);
end--;
}
}
//向下调整算法
void AdjustDown(int* a, int n, int root)
{
int parent = root;
int child = parent * 2 + 1;
while (child < n)
{
if (child + 1 < n && a[child + 1] > a[child])
{
child++;
}
if (a[child] > a[parent])
{
Swap(&a[child], &a[parent]);
parent = child;
child = parent * 2 + 1;
}
else
{
break;
}
}
}
堆排序特点总结:
1.时间复杂度为O(N*logN)
2.空间复杂度为O(1)
3.稳定性:不稳定。使用了堆排序,因此结构不稳定。
3.堆排序对数据不敏感,无论有没有序都要进行堆排序。
5.交换排序
交换排序的核心思想是选取每一位数,与前面的数进行交换判定,如果比他大则发生交换,比他小则证明已经有序,换下一个数。
步骤如下:
1.用end记录冒泡的最终位置。
2.序列中的数据进行两两比较和交换,一直到end。
3.如果过程中没有发生交换则结束循环。
4.end--,继续冒下一趟泡。
动画演示:
代码实现:
//交换排序
//时间复杂度为O(N^2)
void BubbleSort(int* a, int n)
{
int end = n;
while (end > 0)
{
int exchange = 0;
for (int i = 1; i < end; i++)
{
if (a[i - 1] > a[i])
{
Swap(&a[i - 1], &a[i]);
exchange = 1;
}
//如果一趟冒泡的过程没有发生交换,则前部分已经有序,不需要继续冒泡
if (exchange== 0)
{
break;
}
}
end--;
}
}
交换排序的特点总结:
1.时间复杂度O(N)~O(N^2)
2.空间复杂度O(1)
3.稳定性:稳定
4.对数据敏感,序列越有序效率越高。
6.快速排序
快速排序的结构类似于二叉树的结构,通过一趟排序将序列分割成两部分,以调整后的中间值key为基准,左边比他小,右边比他大,之后无限将其分割,以达到排序的目的。
1.左右指针法
步骤如下:
1.选取一个基准值,可以是最左边(begin)也可以是最右边(end)。
2.根据选取的位置确定谁先走,如果选的是左边,则右边先走,如果选的是右边,则左边先走(之后解释)。因为左边选取的是比key小的数,所以遇到比key大的数时需要停下;右边则是选取比key大的数因此遇到比key小的数时需要停下。当左右两边都停下后,就可以将二者交换。
3.重复2步骤,直到序列走完。当序列走完时begin和end相遇,把基准值赋值给当前位置。
动画演示:
代码实现:
//单趟快速排序
//综合情况看,快排的时间复杂度是O(logN*N)
//1.左右指针法
int PartSort1(int* a, int begin, int end)
{
int midIndx = GetMidIndex(a, begin, end);//取中位数,保证不是最坏的情况,不会找到最大或
者最小的情况。
Swap(&a[midIndx], &a[end]);
int keyindex = end;
while (begin < end)
{
//bgein先走,bgein找比key大的数,找到则停下
while (begin < end && a[begin] <= a[keyindex])
{
begin++;
}
//end后走,end找比key小的数,找到则停下
while (begin < end && a[end] >= a[keyindex])
{
end--;
}
//此时end走到了比key小的位置,bgein走到了比key大的位置,让二者进行交换
Swap(&a[end], &a[begin]);
//结束后在继续走,直到把序列走完
}
Swap(& a[begin], & a[keyindex]);
return begin;
}
解释选右走左,选左走右:当基准值定为最右边的时候,需要左边先走寻找比key大的数,之后右边再走寻找比key小的值,最后两者交换。几个回合后,begin和end最终会相遇,当左边先走,那就是begin遇到end,key赋值给end的位置,这样可以保证key比左边的数都要大,比右边的数都要小,可以达成排序的目的;假如是右边先走,最后begin和end仍然相遇,但是会是end遇begin,begin此时的值比key要小,当与end相遇发生交换时,会把这个小的数交换到右边去,导致右边出现比key小的数,不能完成排序,因此必须让begin遇end才行。
左右指针的特点:
1.时间复杂度为O(N*logN)
2.空间复杂度为O(1)
2.挖坑法
挖坑法相对于左右指针法更好理解,二者殊途同归,过程也是类似。核心思路是将原先的基准值定位坑,begin从左边开始找大,填到右边的坑;end从右边开始找小,填到左边的坑;最后左右相遇,把基准值填到相遇的地方。
步骤如下:
1.确定最左或者最右为开始的坑位key
2.左边开始寻找比key大的数,找到后把其填到右边的坑位;右边开始寻找比key小的数,找到后把其填到左边的坑位。
3.在begin和end相遇的地方把key填到此处。
动画演示:
代码实现:
//2.挖坑法
int PartSort2(int* a, int begin, int end)
{
int midIndx = GetMidIndex(a, begin, end);
Swap(&a[midIndx], &a[end]);
int key = a[end];
//最开始的坑
while (begin < end)
{
while (begin < end && a[begin] <= key)
{
begin++;
}
//左边已经找到比key大的坑,把begin位置的数填到右边,begin的位置形成新的坑
a[end] = a[begin];
while (begin<end && a[end] >= key)
{
end--;
}
//右边已经找到比key小的坑,把end位置的数填到左边,end的位置形成新的坑
a[begin] = a[end];
}
//begin和end相遇,把key填到二者相遇的地方
a[begin] = key;
return begin;
}
挖坑法特点总结:
1.时间复杂度为O(N*logN)
2.空间复杂度为O(1)
3.思路简单直观,便于理解
3.前后指针法
前后指针法通过定义两个指针,cur在前,prev在后,仍然需要找到一个基准值key,cur向前寻找,找到目标后和prev++交换,最后把prev和key交换。
步骤如下:
1.设置两个指针,前指针cur(赋值为开头begin的位置),后指针prev(begin-1)。再选定一个基准值key
2.当cur小于key时,cur和prev的值发生交换,prev++后会发生与cur相等的情况,此时可以不交换。当cur大于key时,prev不变cur++。
3.循环第二步,直到cur到达end时,此时将++后的prev与key交换即可。
动画演示:
代码实现:
//3.前后指针法
int PartSort3(int* a, int begin, int end)
{
int cur = begin;
int prev = begin - 1;
int keyIndx = end;
while (cur < end)
{
if (a[cur] < a[keyIndx]&& ++prev!=cur)//当cur小于key时,cur和prev发生交换,prev++后会
发生与cur相等的情况,此时可以不交换
{
Swap(&a[cur], &a[prev]);
}
cur++;
}
Swap(&a[++prev], &a[keyIndx]);
return prev;
}
4.非递归栈实现快速排序
递归改非递归的方法大概分为两种,一种是改为循环,一些简单的递归是可以改成循环实现,另一种方式是改为由栈实现,利用栈的存储来模拟递归。
步骤如下
1.构建栈,将最右侧和最左侧依次压入栈内
2.进入循环,如果栈是非空则开始循环
3.每次循环要取出子区间,将子区间再循环处理
4.销毁栈
代码实现
void QuickSortNonR(int* a, int left, int right)
{
Stack st;
StackInit(&st);
//入栈
StackPush(&st, right);
StackPush(&st, left);
while (!StackEmpty(&st))
{
int begin = StackTop(&st);
StackPop(&st);
int end= StackTop(&st);
StackPop(&st);
//此时取出[begin,end]
int div = PartSort3(a, begin, end);
//[begin,div-1] div [div+1,end]
//先处理右
if (div + 1 < end)
{
StackPush(&st, end);
StackPush(&st, div+1);
}
//再处理左
if (begin < div - 1)
{
StackPush(&st, div-1);
StackPush(&st, begin);
}
}
StackDestory(&st);
}
7.归并排序
归并排序与合并有序数组的思想类似,将数组中的数据依次拿下来,直到数据为1,然后与其他数排序,当成两个数组合并,最后归并到一起,完成排序
1.拆分区间,将序列拆分成左右两个区间,确定先归并哪边
2.递归拆分的过程,直到序列被拆分成一个一个的
3.开始归并,按合并两个有序数组那样进行排序,利用开辟的一块空间容纳排序的序列数据,之后再拷回原序列
图示说明:
动画演示:
代码实现:
void MergeArr(int* a, int begin1, int end1, int begin2, int end2, int* tmp)
{
int left = begin1, right = end2;
int index = begin1;
while (begin1 <= end1 && begin2 <= end2)
{
if (a[begin1] < a[begin2])
{
tmp[index++] = a[begin1++];
}
else
{
tmp[index++] = a[begin2++];
}
}
while (begin1 <= end1)
{
tmp[index++] = a[begin1++];
}
while (begin2 <= end2)
{
tmp[index++] = a[begin2++];
}
//把归并好的tmp再拷回原数组
for (int i = left; i <= right; i++)
{
a[i] = tmp[i];
}
}
//时间复杂度是O(N*logN)
//空间复杂度O(N)
void _MergeSort(int* a, int left, int right, int* tmp)
{
if (left >= right)
{
return;
}
int mid = (left + right) / 2;
//[left,mid][mid+1,right]有序则可以合并,现在没有序,变成子问题解决
_MergeSort(a, left, mid, tmp);
_MergeSort(a, mid + 1, right, tmp);
MergeArr(a, left, mid, mid + 1, right,tmp);
}
//归并排序递归实现
void MergeSort(int* a, int n)
{
assert(a);
int* tmp = malloc(sizeof(int) * n);
_MergeSort(a, 0, n - 1, tmp);
free(tmp);
}
归并排序特点总结:
1.时间复杂度是O(N*logN)
2.空间复杂度O(N)3.类似于二叉树的结构,数据稳定