网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。
一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!
解决方案:
交换一次后判断max是不是begin,是的话max的值就是min。同理,反过来也成立。
直接选择排序的特性总结****🚀
直接选择排序思考非常好理解,但是效率不是很好。实际中很少使用
时间复杂度:
O(N^2)
空间复杂度:
O(1)
稳定性:不稳定
**2.2.3 堆排序(点我看前面堆详解博客)**🚀
堆排序
(Heapsort)
是指利用堆积树(堆)这种数据结构所设计的一种排序算法,它是选择排序的一种。它是通过堆来进行选择数据。需要注意的是排升序要建大堆,排降序建小堆。直接选择排序的特性总结****🚀
堆排序使用堆来选数,效率就高了很多。
时间复杂度:
O(N*logN)
空间复杂度:
O(1)
稳定性:不稳定
直接选择和堆排序的实现 🚀
我们这里是选了两个数,相较于选一个优化了
2.3 交换排序****🚀
2.3.1基本思想🚀
所谓交换,就是根据序列中两个记录键值的比较结果来对换这两个记录在序列中的位置,交换排序的特点是:将键值较大的记录向序列的尾部移动,键值较小的记录向序列的前部移动。
**2.3.2冒泡排序(属于老朋友了是哈哈)**🚀
冒泡排序的特性总结 🚀
冒泡排序是一种非常容易理解的排序
时间复杂度:
O(N^2)
空间复杂度:
O(1)
稳定性:稳定
**2.3.3 快速排序(也算是老朋友了,qsort大家应该用挺多的哈)**🚀
快速排序是
Hoare
于
1962
年提出的一种二叉树结构的交换排序方法,其基本思想为:
任取待排序元素序列中
的某元素作为基准值,按照该排序码将待排序集合分割成两子序列,左子序列中所有元素均小于基准值,右
子序列中所有元素均大于基准值,然后最左右子序列重复该过程,直到所有元素都排列在相应位置上为止
。注意,这里讲的主要是思想,图和PartSort代码没有关系,画的时候只追求了
比key大的在一边,比key小的在另一边这个条件。这张图主要是展示边界控制的问题。
快速排序有的讲,这里有四种实现方法
// 假设按照升序对array数组中[left, right)区间中的元素进行排序 void QuickSort(int array[], int left, int right) { if(right - left <= 1) return; // 按照基准值对array数组的 [left, right)区间中的元素进行划分 int div = partion(array, left, right); // 划分成功后以div为边界形成了左右两部分 [left, div) 和 [div+1, right) // 递归排[left, div) QuickSort(array, left, div); // 递归排[div+1, right) QuickSort(array, div+1, right); }
上述为快速排序递归实现的主框架,发现与二叉树前序遍历规则非常像,同学们在写递归框架时可想想二叉树前序遍历规则即可快速写出来,后序只需分析如何按照基准值来对区间中数据进行划分的方式即可。
将区间按照基准值划分为左右两半部分的常见方式有:
1. hoare版本****🚀
为什么不能第一个开始左指针先走呢?
第一个开始左指针走之前不会有什么问题,但是到了最后一步左指针会找到比key值大的数,然后停下和key交换。
而如果是右指针的话,它找到的是比key小的数,交换不会出现任何问题。最后一个开始右指针先走同理。
2. 挖坑法****🚀
我们先看一下单次排序的动图:
顺着来就好,不像hoare版本需要考虑一些小小的细节。
3. 前后指针版本****🚀
我们先来看一下单趟动图:
这个方法的核心就是保证prev指向及prev之前的所有数据的值都小于key。
- 当cur还没遇见比key大的值的时候,prev是跟着cur一起移动的。
- 当cur第一次遇见比key大的值的时候,prev停下,此时prev包括prev前的数据都是小于key的。
- 当cur再一次遇见比key小的数据时,prev的下一个一定是比key大的数据,所以prev++和cur交换。
- 直到遍历结束。prev之前包括prev都比key小,key和prev交换。
- key之前都比key小,key之后都比key大。
然后我们来考虑一下key取最右边:
为了保证prev包括prev前的数据都是小于key的。 prev就不能从0位置开始了,万一第一个数就大于key呢?接下来的路与取左边完全一样,直到cur在key位置的时候:
prev包括prev前的数据都是小于key的。(哇,这话我说了多少次)
- 在左边的时候prev前面有key,所以可以直接交换。
- 在右边的时候直接交换会把小的数换到右边,所以交换的时候是换prev++的位置。
4.非递归法****🚀
2.3.3 快速排序优化****🚀
三数取中法选
key
递归到小的子区间时,可以考虑使用插入排序(因为已经趋近有序,直接插入会非常快)
快速排序的特性总结****🚀
快速排序整体的综合性能和使用场景都是比较好的,所以才敢叫
快速
排序
时间复杂度:
O(N*logN)
空间复杂度:
O(logN)(因为是递归,所以高度是logN,而每次开辟O(1)个空间,相乘就是logN)
稳定性:不稳定
2.4 归并排序****🚀
2.4.1 基本思想****🚀
归并排序(
MERGE-SORT
)是建立在归并操作上的一种有效的排序算法
,
该算法是采用分治法(
Divide andConquer)的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为二路归并。 归并排序核心步骤:2.4.2 递归实现 🚀
2.4.3 非递归实现 🚀
归并排序的特性总结****🚀
归并的缺点在于需要
O(N)
的空间复杂度,归并排序的思考更多的是解决在磁盘中的外排序问题。
时间复杂度:
O(N*logN)
空间复杂度:
O(N)
稳定性:稳定
3.补充非比较排序——计数排序 🚀
比如我们有个单词Hippopotomonstrosesquippedaliophobia
我们想把它的字母排个序,怎么排好呢?
根据观察,这个单词里面含有大量重复的字母,我们是不是可以数一下这些字母出现的次数, 然后按顺序写下来就好了?
计数排序的原理就是这个,它对于大量且集中的数据有奇效。
思路:
统计相同元素出现次数 。
根据统计的结果将序列回收到原来的序列中。
网上动图
有负数也可以哈,因为这里用的相对位置,不是绝对位置。
代码
//用相对映射,不用绝对映射 void CountSort(int* a, int n) { // 1 2 2 4 3 2 5 int min = a[0]; int max = a[0]; int i = 0; for (i = 0; i < n; i++) { if (a[i] > max) max = a[i]; if (a[i] < min) min = a[i]; } int range = max - min + 1; int* countA = (int*)calloc(range,sizeof(int)); assert(countA); //计数 //5000 0 /// 100 150 200 210 250 0-151 min 100 for (i = 0; i < n; i++) { countA[a[i] - min]++; } //排序 int j = 0; for (i = 0; i < range; i++) { while (countA[i]--) { a[j++] = i + min; } } }
4.排序算法复杂度及稳定性分析****🚀
5.源代码 🚀
Sort.h 🚀
#define _CRT_SECURE_NO_WARNINGS 1
#pragma once
#include<stdio.h>
#include<stdlib.h>
#include<time.h>
#include<assert.h>
#include<stdlib.h>
#include<string.h>
#include"Stack.h"
void Print(int* arr, int sz);
//2
void InsertSort(int* arr, int sz);
//3
void ShellSort(int* arr, int sz);
//1
void BubbleSort(int* arr, int sz);
//4
void SelectSort(int* arr, int sz);
//6
void HeapSort(int* arr, int sz);
//5
void QuickSort(int* arr, int begin, int end);
void QuickSortNonR(int* arr, int begin, int end);
//7
void MergeSort(int* arr, int begin, int end);
void MergeSortNonR(int* arr, int n);
Stack.h 🚀
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
#include<stdbool.h>
typedef int STDateType;
typedef struct Stack {
STDateType* arr;
int top;
int capacity;
}Stack;
//初始化
void InitStack(Stack* pst);
//入栈
void PushStack(Stack* pst, STDateType x);
//出栈
void PopStack(Stack* pst);
//取栈顶
STDateType StackTop(Stack* pst);
//判空
bool EmptyStack(Stack* pst);
//求数据个数
int StackSize(Stack* pst);
//销毁
void DestoryStack(Stack* pst);
Stack.c 🚀
#define _CRT_SECURE_NO_WARNINGS 1
#include"Stack.h"
//初始化
void InitStack(Stack* pst)
{
//int newcapacity = pst->capacity = 0 ? 4 : pst->capacity * 2;
Stack* new = (Stack*)malloc(sizeof(Stack));
if (new == NULL)
{
perror("InitStack:");
exit(-1);
}
pst->arr = new;
pst->capacity = 1;
pst->top = 0;
}
//入栈
void PushStack(Stack* pst, STDateType x)
{
assert(pst);
if (pst->capacity == pst->top)
{
int newcapacity = pst->capacity * 2;
STDateType* new = (STDateType*)realloc(pst->arr,sizeof(STDateType) * newcapacity);
if (new == NULL)
{
perror("InitStack:");
exit(-1);
}
pst->arr = new;
pst->capacity = newcapacity;
}
pst->arr[pst->top] = x;
pst->top++;
}
//出栈
void PopStack(Stack* pst)
{
assert(pst);
assert(pst->top > 0);
pst->top--;
}
//取栈顶
STDateType StackTop(Stack* pst)
{
assert(pst);
assert(pst->top > 0);
return pst->arr[pst->top - 1];
}
//判空
bool EmptyStack(Stack* pst)
{
assert(pst);
return pst->top == 0;
}
//求数据个数
int StackSize(Stack* pst)
{
assert(pst);
return pst->top;
}
//销毁
void DestoryStack(Stack* pst)
{
assert(pst);
free(pst->arr);
pst->capacity = 0;
pst->top = 0;
}
Sort.c 🚀
#define _CRT_SECURE_NO_WARNINGS 1
#include"Sort.h"
//后面用的多,直接写成函数方便一点
void Swap(int* p, int* q)
{
int tmp = *p;
*p = *q;
*q = tmp;
}
void Print(int* arr, int sz)
{
for (int i = 0; i < sz; i++)
{
printf("%d ", arr[i]);
}
printf("\n");
}
//思想:冒泡其实没啥说的,注意第二个循环的-1,因为我们
//这里用的是arr[j]和arr[j+1]小心越界的问题
void BubbleSort(int* arr,int sz)
{
int i = 0;
int j = 0;
for (i = 0; i < sz; i++)
{
for (j = 0; j < sz-i-1; j++)
{
if (arr[j] > arr[j + 1])
Swap(&arr[j], &arr[j + 1]);
}
}
}
//思想:开始让第一个数有序,然后从后一个数插入,
//如果后一个数小于前面的数,前面的数就往后挪动
//注意不管是哪种情况退出,最后都是把数据放到
//end+1,所有干脆我们就放在外面
void InsertSort(int* arr, int sz)
{
for(int i = 0; i < sz - 1;i++)
{
int end = i;
int tmp = arr[end + 1];
while (end >= 0)
{
if (arr[end] > tmp)
{
arr[end + 1] = arr[end];
end--;
}
else
{
break;
}
}
arr[end + 1] = tmp;
}
}
//思想:先对数组进行预排序,预排序是为了
//直接插入更快,越有序,直接插入就越快,这
//也是希尔快的原因,预排序完了就直接插入排序
//预排序:分组(离相同gap)为一组进行插入排序
void ShellSort(int* arr, int sz)
{
// 1、gap > 1 预排序
// 2、gap == 1 直接插入排序
int gap = sz;
while (gap>1)
{
//保证最后为直接插入排序
gap = gap / 3 + 1;
//这里i++是使多组预排序同时进行
for (int i = 0; i < sz - gap; i++)
{
int end = i;
int tmp = arr[end + gap];
while (end >= 0)
{
if (tmp < arr[end])
{
arr[end + gap] = arr[end];
end -= gap;
}
else
{
break;
}
}
arr[end + gap] = tmp;
}
}
}
//思路:选两个值的下标,开始都在左边,然后用一个记录下标的cur去遍历数组,
// 一个记录最大(maxi),一个记录最小(mini),然后遍历数组把最小的放
// 在左边,最大的,放在右边,直到left,right相遇
void SelectSort(int* arr, int sz)
{
int left = 0;
int right = sz - 1;
while (left<right)
{
int cur = left+1;
int mini = left;
int maxi = left;
//这里注意要等于哈,不然最后一个值就比掉了
//作者开始写的时候就画图搞了半天QAQ
while (cur <= right)
{
if (arr[cur] > arr[maxi])
{
maxi = cur;
}
if (arr[cur] < arr[mini])
{
mini = cur;
}
cur++;
}
Swap(&arr[left], &arr[mini]);
//如果left==maxi left就会
//和mini交换,要更新maxi
if (left == maxi)
{
maxi = mini;
}
Swap(&arr[right], &arr[maxi]);
left++;
right--;
}
}
//hoare
//思路:左边为key,右边先走,找小,左边再走,找大,然后左右交换
//一直循环直到相遇,因为是右边先走,左边已经是找小
//停住了,所有能保证相遇位置大于等于a[jeyi],然后
//交换a[keyi]和相遇位置的值,这样a[keyi]左边值就
//比他小,右边值就比他大
int PartSort1(int* arr, int left, int right)
{
int keyi = left;
while (left < right)
{
//右找小 注意:keyi在左边就右边先走
//要注意left>right的条件,不然可能越界
while (left < right && arr[right] >= arr[keyi])
{
right--;
}
//左找大
while (left < right && arr[left] <= arr[keyi])
{
left++;
}
Swap(&arr[left], &arr[right]);
}
Swap(&arr[left], &arr[keyi]);
return left;
}
//挖坑法
//思路:左边为key,先存起来,变成坑,左右两边一个变量
//右边先走,找小,然后交换,左边找大,然后交换
//最后把key的值给相遇位置
int PartSort2(int* a, int left, int right)
{
int pi = left;
int qi = right;
int key = a[left];
while (pi < qi)
{
//右找小
while (a[qi] > key && pi < qi)
{
qi--;
}
a[pi] = a[qi];
//左找大
while (a[pi] < key && pi < qi)
{
pi++;
}
a[qi] = a[pi];
}
a[qi] = key;
return qi;
}
int GetMidIndex(int* a, int left, int right)
{
//int mid = (left + right) / 2;
int mid = left + (right - left) / 2;
// left mid right
if (a[left] < a[mid])
{
if (a[mid] < a[right])
{
return mid;
}
else if (a[left] > a[right])
{
return left;
}
else
{
return right;
}
}
else // a[left] > a[mid]
{
if (a[mid] > a[right])
{
return mid;
}
else if (a[left] < a[right])
{
return left;
}
else
{
return right;
}
}
}
//前后指针法
//左边为keyi,用两个变量存储left和left+1,cur去找小,
//找到了prev++再交换然后cur++,没有找到就cur++,
//这样可以保证让prev到cur的值都大于等于a[keyi]
//最后交换a[keyi]和a[prev]
int PartSort3(int* a, int left, int right)
{
//yysy,自己实现不加也行
//下面这两句也是优化,处理了有序O(N^2)的情况
int midi = GetMidIndex(a, left, right);
Swap(&a[midi], &a[left]);
int cur = left + 1;
int prev = left;
int keyi = left;
while (cur <= right)
{
//这一步很巧哈,可以多理解一下
//注意是前置++
if (a[cur] < a[keyi] && a[++prev] != a[cur])
{
Swap(&a[cur], &a[prev]);
}
cur++;
}
Swap(&a[keyi], &a[prev]);
return prev;
}
//递归
void QuickSort(int* arr, int begin, int end)
{
// 子区间相等只有一个值或者不存在那么就是递归结束的子问题
if (begin >= end)
return;
//小区间优化
// 小区间直接插入排序控制有序
//yysy,自己实现不加也行
if (end - begin + 1 <= 10)
{
InsertSort(arr + begin, end - begin + 1);
}
else
{
int keyi = PartSort3(arr, begin, end);
// [begin, keyi-1]keyi[keyi+1, end]
QuickSort(arr, begin, keyi - 1);
QuickSort(arr, keyi + 1, end);
}
}
//非递归
void QuickSortNonR(int* arr, int begin, int end)
{
Stack st;
InitStack(&st);
PushStack(&st, begin);
PushStack(&st, end);
while (!EmptyStack(&st))
{
int right = StackTop(&st);
PopStack(&st);
int left = StackTop(&st);
PopStack(&st);
//left keyi-1 keyi+1 right
int keyi = PartSort1(arr, left, right);
if (left < keyi-1)
{
PushStack(&st, left);
PushStack(&st, keyi-1);
}
if (keyi + 1 < right)
{
PushStack(&st, keyi + 1);
PushStack(&st, right);
}
}
}
//升序建大堆
先找左右小的孩子
//再和root的比 比root小就交换
//root等于那个要交换的孩子 孩子再选 然后迭代
void AdjustDown(int* arr,int root,int sz)
{
//默认左孩子
int child = root * 2 + 1;
int parent = root;
while (child<sz)
{
1、选出左右孩子中大的那个
if (child + 1 < sz && arr[child + 1] > arr[child])
child++;
//2、如果孩子大于父亲,则交换,并继续往下调整
if (arr[child] > arr[parent])
{
Swap(&arr[parent], &arr[child]);
parent = child;
child = parent * 2 + 1;
}
else
{
break;
}
}
}
//思想:用向下调整算法建大堆(升序),然后把堆顶的数据
//换到最后,然后缩小数据范围,再次向下调整,反复如此
void HeapSort(int* arr, int sz)
{
//向下调整--建大堆 O(N)
int curpos = (sz - 1 - 1) / 2;
while (curpos >= 0)
{
AdjustDown(arr, curpos, sz);
curpos--;
}
//end为最后一个元素下标
int end = sz - 1;
while (end >0)
{
Swap(&arr[0], &arr[end]);
//每次从0开始向下调
AdjustDown(arr, 0, end);
end--;
}
}
void _MergeSort(int* arr, int* tmp, int begin, int end)
{
if (begin >= end)
return;
//先分后合
//为了防止数据溢出
int mid = begin + (end - begin) / 2;
//注意这里我们分成 begin - mid 和 mid+1 - end
//分成 begin - mid-1 和 mid - end 有bug (1,2)会死循环
int begin1 = begin; int end1 = mid;
int begin2 = mid+1; int end2 = end;
_MergeSort(arr, tmp, begin1, end1);
_MergeSort(arr, tmp, begin2, end2);
//printf("begin1:%d end1:%d \nbegin2:%d end2:%d\n", begin1, end1, begin2, end2);
//开始归
int i = begin;
while (begin1 <= end1 && begin2 <= end2)
{
if (arr[begin1] < arr[begin2])
tmp[i++] = arr[begin1++];
else
tmp[i++] = arr[begin2++];
}
while(begin1 <= end1)
tmp[i++] = arr[begin1++];
while (begin2 <= end2)
tmp[i++] = arr[begin2++];
//注意第三个参数是字节大小.....开始还以为是上面错了,找半天
//注意要加上begin 因为并不是每次都是初始位置
memcpy(arr + begin, tmp + begin, (end - begin + 1)*sizeof(int));
}
//思想:先分再合
//注意我们要先把排号的数据先放进tmp数组,
//然后把数组的数据拷回arr
void MergeSort(int* arr, int begin, int end)
{
int* tmp = (int*)malloc((end-begin+1) * sizeof(int));
assert(tmp);
//避免重复开辟空间就用副本函数
_MergeSort(arr, tmp, begin, end);
free(tmp);
}
//思想:不分,直接和,注意和快排转非递归不一样,快排是前序,
//所以转非递归用栈和队列都可以,但是后续不好那么搞,
//我们直接暴力求
void MergeSortNonR(int* arr, int n)
{
int* tmp = (int*)malloc(n * sizeof(int));
assert(tmp);
int gap = 1;
int begin1 = 0; int end1 = 0;
int begin2 = 0; int end2 = 0;
while (gap < n)
{
for (int i = 0; i < n; i += 2 * gap)
{
![img](https://img-blog.csdnimg.cn/img_convert/4ad9cff86c0d432f030e6aee6a3d847c.png)
![img](https://img-blog.csdnimg.cn/img_convert/11b6bb802b9b2ab2e62b18c49ec284fb.png)
**网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。**
**[需要这份系统化的资料的朋友,可以添加戳这里获取](https://bbs.csdn.net/topics/618668825)**
**一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!**
in1 <= end1)
tmp[i++] = arr[begin1++];
while (begin2 <= end2)
tmp[i++] = arr[begin2++];
//注意第三个参数是字节大小.....开始还以为是上面错了,找半天
//注意要加上begin 因为并不是每次都是初始位置
memcpy(arr + begin, tmp + begin, (end - begin + 1)*sizeof(int));
}
//思想:先分再合
//注意我们要先把排号的数据先放进tmp数组,
//然后把数组的数据拷回arr
void MergeSort(int* arr, int begin, int end)
{
int* tmp = (int*)malloc((end-begin+1) * sizeof(int));
assert(tmp);
//避免重复开辟空间就用副本函数
_MergeSort(arr, tmp, begin, end);
free(tmp);
}
//思想:不分,直接和,注意和快排转非递归不一样,快排是前序,
//所以转非递归用栈和队列都可以,但是后续不好那么搞,
//我们直接暴力求
void MergeSortNonR(int* arr, int n)
{
int* tmp = (int*)malloc(n * sizeof(int));
assert(tmp);
int gap = 1;
int begin1 = 0; int end1 = 0;
int begin2 = 0; int end2 = 0;
while (gap < n)
{
for (int i = 0; i < n; i += 2 * gap)
{
[外链图片转存中...(img-Mo74gdqe-1715827145673)]
[外链图片转存中...(img-SGO8EvAJ-1715827145673)]
**网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。**
**[需要这份系统化的资料的朋友,可以添加戳这里获取](https://bbs.csdn.net/topics/618668825)**
**一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!**