网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。
一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!
}
}
}
//堆排
void HeapSort(int* arr, int n)
{
for(int i = (n - 1 - 1) / 2 ; i >= 0; i–)
{
AdjustDown(arr, n, i);
}
int end = n - 1;
while (end >= 0)
{
Swap(&arr[end--],&arr[0]);
AdjustDown(arr,end,0);
}
}
### 冒泡排序

冒泡排序的基本思想:
就是根据序列中两个记录键值的比较结果来对换这两个记录在序列中位置,交换排序的特点是:将键值较大的记录向序列的尾部移动,键值较小的记录向序列的前部移动。

两两相比较,将小的交换到前面大的交换到后面,排序n个元素只需要比较n - 1趟,每冒泡一趟少比较一个元素
//冒泡排序
void bubblesort(int* arr, int n)
{
int end = 0;
for (end = n; end > 0; end–)
{
int flag = 0;
int j = 0;
for (j = 1; j < end; j++)
{
if (arr[j - 1] > arr[j])
{
Swap(&arr[j - 1] ,&arr[j]);
flag = 1;
}
else
{
break;
}
}
if (!flag)
break;
}
}
1. 时间复杂度:O(N^2)
2. 空间复杂度:O(1)
3. 稳定性:稳定
### 快排
快速排序是Hoare于1962年提出的一种二叉树结构的交换排序方法,其基本思想为:任取待排序元素序列中的某元素作为基准值,按照该排序码将待排序集合分割成两子序列,左子序列中所有元素均小于基准值,右子序列中所有元素均大于基准值,然后最左右子序列重复该过程,直到所有元素都排列在相应位置上为止。
#### hoare版本
过程:
1、单趟排序选出key,通常情况下key的位置都是选择在数组下标为0的位置,最左边,最右边都可以
2、将小的值交换到左边,将大的值交换到右边,最终把key放到正确的位置,保证左边的值要比key小,右边的值要比key大
左右指针法:
左边的哨兵找比key大的值,右边的哨兵找比key小的值


观察到的现象是一次单趟排序后,key的左边值都比key要小,key右边的值都要比key大,这样就已经达成了初步有序的目的了

if (begin >= end)
{
return;
}
int left = begin, right = end;
int key = left;
while (left < right)
{
//右找小,left < right防止升序的情况下出现越界
while (left < right && arr[right] >= arr[key])
{
right--;
}
//左找大
while (left < right && arr[left] <= arr[key])
{
left++;
}
//交换,将比key小的值换到左边,比key大的值换到右边
Swap(&arr[left], &arr[right]);
}
int meet = left;
//确定key的位置
Swap(&arr[left], &arr[key]);
QuickSort(arr,begin, meet - 1);
QuickSort(arr, meet + 1, end);
#### 挖坑法
基本思想:
将第一个位置选做key,这样就形成了天然的坑位,右边去找比key小的值,找到后将值填充到坑位中,自己成为新坑,左去找大,找到后将值放到右边的坑中,自己成为新的坑,反复直到相遇,相遇点也是一个坑位,将key的值放到坑中,这样单趟排就已经确定key值的位置了


>
> 初步单趟排就已经确定了,并且key也放到它正确的位置了,key的左边比他小,右边比他大
>
>
>
//挖坑法
void QuickSort(int* arr, int begin, int end)
{
if (begin >= end)
{
return;
}
int left = begin, right = end;
//将第一个数据存放在临时变量中,成为一个天然坑位
int key = arr[left];
while (left < right)
{
//找小
while (left < right && arr[right] >= key)
right--;
//找到小的将小的放到左边坑,右边成为新的坑位
arr[left] = arr[right];
//找大
while (left < right && arr[left] <= key)
left++;
//找到大的将大的放到右边坑,左边成为新的坑位
arr[right] = arr[left];
}
//选key,将key值放到相遇点的位置
arr[left] = key;
int meet = left;
//左区间单趟排
QuickSort(arr, begin,meet - 1);
//右区间单趟排
QuickSort(arr, meet + 1, end);
}
#### 前后指针法
基本思想:
双指针,定义prev和cur开始一前一后,cur去找比key小的值,找到后++prev,再交换cur和prev它们的值,直到数组遍历完,最后一下交换key位置的值和prev位置的值,这样就确定了key值的位置


//前后指针法
void __QuickSort(int* arr, int begin, int end)
{
if (begin >= end)
{
return;
}
int prev = begin - 1;
int cur = begin;
int key = begin;
while (cur <= end)
{
while (arr[cur] < arr[key] && ++prev != cur)
{
Swap(&arr[cur], &arr[prev]);
}
cur++;
}
Swap(&arr[prev],&arr[key]);
\_QuickSort(arr, begin, prev - 1);
\_QuickSort(arr, prev + 1, end);
}
#### 快排时间复杂度分析:
先考虑理想情况:

从图中我们可以看出快排的单趟排不管是右遇左,还是左遇右,合计起来也只能走数组长度N次,每选一个key值划分左右区间,单趟排确定key值的位置,不断递归划分左右区间,递归的深度是以2^N递增的,所以它的时间复杂度是O(N \* log N)
考虑最坏情况:
如果数组已经是有序的了,那么不管是右遇左,还是左遇右每次都得走n-1步才能选出key的位置,那么它的执行次数就是一个等差数列了,所以时间复杂度就是O(N^2),怎么优化呢?

#### 快排的优化:
针对快排得优化:
思考:对快排影响最大的是选的key,如果key越接近中位数越接近二分,效率越高
#### 1、三数取中
>
> 找出这个区间中的中位数,使每次选key都为中位数,那么不用考虑有序这种恶劣的情况出现了
>
>
>
//三数取中
int GetMidIndex(int* arr, int left, int right)
{
int mid = (left + right) >> 1;
if (arr[left] < arr[mid])
{
if (arr[mid] < arr[right])
{
return mid;
}
else if (arr[left] > arr[right])
{
return left;
}
else
{
return right;
}
}
else //arr[left] > arr[mid]
{
if (arr[mid] > arr[right])
{
return mid;
}
else if (arr[left] < arr[right])
{
return left;
}
else
{
return right;
}
}
}
#### 2、小区间优化
>
> 当每一个区间递归下去的时候只剩20个数了(官方参考),就可以考虑不再递归换用插入排序,接近于有序插入排序的效果会更好一些,而且递归 也是有消耗的,能节省就节省一些
>
>
>
if (end - begin > 10)
{
QuickSort(arr, begin, meet - 1);
QuickSort(arr, meet + 1, end);
}
else
{
InsertSort(arr + begin, end - begin + 1);
}
**完整的代码:**
//快速排序
void QuickSort(int* arr, int begin, int end)
{
if (begin >= end)
{
return;
}
int MidIndex = GetMidIndex(arr, begin, end);
int left = begin, right = end;
Swap(&arr[MidIndex], &arr[left]);
int key = left;
while (left < right)
{
while (left < right && arr[right] >= arr[key])
{
right--;
}
while (left < right && arr[left] <= arr[key])
{
left++;
}
Swap(&arr[left], &arr[right]);
}
int meet = left;
Swap(&arr[left], &arr[key]);
if (end - begin > 20)
{
QuickSort(arr, begin, meet - 1);
QuickSort(arr, meet + 1, end);
}
else
{
InsertSort(arr + begin, end - begin + 1);
}
}
#### 快排非递归实现
>
> 为什么会有非递归的版本呢,有些场景递归解决不了的问题于是就需要非递归登场了
> 非递归实现思想:由于C语言库并没有栈,所以需要自己动手实现一个栈,如果需要相关代码的可以点击这个链接: [栈实现](https://bbs.youkuaiyun.com/forums/4f45ff00ff254613a03fab5e56a57acb).要想实现非递归并不难,只需要理解好快排的递归原理,快排的递归思想是,对一段区间单趟排,选key,确定好key的位置,再对key的左区间递归递归单趟排,key的右区间递归单趟排,不断地分裂,确定key的位置,只剩一个值了,这样一来数组就有序了,读者有没有发现,其中描述的两个步骤无非就是选key和对一段区间进行单趟排序,直到只剩一个值了,就可以是有序的,所以只需要用栈模拟递归的过程,将一段区间用栈保存起来,取区间出来单趟排,再选key的位置,不断划分左右区间,最终只剩一个值了,数组就是有序的了
>
>
>
//单趟排序,返回key
int parsort(int* arr, int begin, int end)
{
//三数取中
int MidIndex = GetMidIndex(arr, begin, end);
int left = begin, right = end;
Swap(&arr[MidIndex], &arr[left]);
int key = left;
while (left < right)
{
//右找小,left < right防止升序的情况下出现越界
while (left < right && arr[right] >= arr[key])
{
right–;
}
//左找大
while (left < right && arr[left] <= arr[key])
{
left++;
}
//交换
Swap(&arr[left], &arr[right]);
}
int meet = left;
Swap(&arr[left], &arr[key]);
return meet;
}
//快排非递归实现
void QuickSortNonR(int* arr, int begin, int end)
{
Stack st;
StackInit(&st);
StackPushBack(&st, begin);
StackPushBack(&st, end);
while (!StackEmpty(&st))
{
//取出右区间
int right = StackTop(&st);
StackPop(&st);
//取出左区间
int left = StackTop(&st);
StackPop(&st);
//单趟排,选出key
int keyi = parsort(arr, left, right);
//入左区间
if (left < keyi - 1)
{
StackPushBack(&st, left);
StackPushBack(&st, keyi - 1);
}
//入右区间
if (keyi + 1 < right)
{
StackPushBack(&st, keyi + 1);
StackPushBack(&st, right);
}
}
}
快排特性总结:
1. 时间复杂度:O(N\*logN)
2. 空间复杂度:O(logN)
3. 稳定性:不稳定
### 归并排序
基本思想:
归并排序(MERGE-SORT)是建立在归并操作上的一种有效的排序算法,该算法是采用分治法(Divide andConquer)的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为二路归并。 归并排序核心步骤:

选出中间的位置,去递归左区间和右区间当它们只剩一个值的时候就不需要再递归,将左右区间中较小的值拷贝到临时数组,再将临时数组的值拷贝会原数组,这样子区间就有序了。
简单点来说就是想让整个区间有序就必须不断地拆分子区间,让子区间先有序了,再来归并子区间让整个区间就有序了。下图用不同的颜色标识每一段区间,当小区间归并后又会替换成另一种新的颜色

>
> 归并和快排的区别:同属于O(N \* log N)时间复杂度的算法,但是归并的空间复杂度却是O(N),因为需要开辟一个额外的数组用来归并使其有序,当然快排是选key再分治递归左右区间,可见他是一种前序遍历思想,根–>左–>右,而归并不同于快排的是他是不断地拆分左右区间,使左右区间只剩一个值了,看成有序,再来对左右区间归并,让这段子区间有序,左–>右–>根所以归并是一种后序思想。
>
>
>
实现代码:
//归并排序
void MergerSort(int* arr, int begin, int end,int *tmp)
{
if (begin >= end)
{
return;
}
int mid = (begin + end) >> 1;
MergerSort(arr,begin, mid,tmp);
MergerSort(arr, mid + 1, end, tmp);
//当左右区间拆分到只有一个值的时候,就可以归并
int begin1 = begin, begin2 = mid + 1;
int i = begin;
while (begin1 <= mid && begin2 <= end)
tmp[i++] = arr[begin1] < arr[begin2] ? arr[begin1++] : arr[begin2++];
while (begin1 <= mid)
tmp[i++] = arr[begin1++];
while (begin2 <= end)
tmp[i++] = arr[begin2++];
int dest = i;
for (i = 0; i < dest; i++)
{
arr[i] = tmp[i];
}
}
#### 归并复杂度分析

可以从图中看到的对N个元素会被分解出N次,每一层都有N个元素,递归的深度是呈logN增长的,所以归并的时间复杂度是O(N \* log N),但是归并唯一遗憾的是它的空间复杂度却是O(N),因为他要开辟一个额外的临时数组。
#### 归并的非递归
>
> 再使用递归的时候由于是后序遍历思想,所以需要将左右区间分解成一个值的时候才开始往回退归并,而现在换用循环就不需要考虑这个区间是不是只剩一个值了,可以通过调整gap来控制排序过程,gap表示的是区间中有几个元素个数,当然这里画的是满了,后面还是一些其他情况也会一 一讲解
>
>
>
控制gap的大小,对这两段区间进行归并,i从0开始
左区间【i ,i + gap - 1】,右区间【i + gap,i + 2 \* gap - 1】

**考虑恶劣的情况**
1、第二个区间并不存在
当gap == 1的时候,但是右区间如果没有的话直接不归跳出循环防止访问越界

如果右区间并不存在

右区间存在但不够gap个,结束位置可能存在越界,需要修正

左区间不够gap个

总结:
1、最后一个小组归并时,第二个小区间不存在,不需要再归并
2、最后一个小组归并时,第二个小区间存在,第二个区间不够gap个
3、最后一个小组归并时,第一个小区间不够gap个,不需要归并。
void _Merger(int* arr, int begin1, int end1,int begin2, int end2, int* tmp)
{
int i = begin1;
while (begin1 <= end1 && begin2 <= end2)
tmp[i++] = arr[begin1] < arr[begin2] ? arr[begin1++] : arr[begin2++];
while (begin1 <= end1)
tmp[i++] = arr[begin1++];
while (begin2 <= end2)
tmp[i++] = arr[begin2++];
int dest = i;
for (i = 0; i < dest; i++)
{
arr[i] = tmp[i];
}
}
//归并非递归实现
void MergerSortNonR(int* arr, int *tmp,int n)
{
int gap = 1;
while (gap < n)
{
for (int i = 0; i < n; i += 2 * gap)
{
int begin1 = i, end1 = i + gap - 1,
begin2 = i + gap, end2 = i + 2 * gap - 1;
//右区间不存在
if (begin2 >= n)
{
break;
}
//右区间存在但是不够gap个
if (end2 >= n)
{
end2 = n - 1;
}
//归并这段区间让他有序
\_Merger(arr,begin1, end1,begin2, end2,tmp);
}
gap \*= 2;
}
}
归并排序特新总结:
1. 归并的缺点在于需要O(N)的空间复杂度,归并排序的思考更多的是解决在磁盘中的外排序问题。
2. 时间复杂度:O(N\*logN)
### 给大家的福利
**零基础入门**
对于从来没有接触过网络安全的同学,我们帮你准备了详细的学习成长路线图。可以说是最科学最系统的学习路线,大家跟着这个大的方向学习准没问题。

同时每个成长路线对应的板块都有配套的视频提供:

因篇幅有限,仅展示部分资料
**网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。**
**[需要这份系统化资料的朋友,可以点击这里获取](https://bbs.youkuaiyun.com/forums/4f45ff00ff254613a03fab5e56a57acb)**
**一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!**