面试常被问到排序算法总结(插入+选择+交换+归并排序)
排序算法总结
谈及排序算法,大家知道常见的排序算法都有哪些呢?
下面来总结一下常见的排序算法:
基本思想:
把待排序的记录按其关键码值的大小逐个插入到一个已经排好序的有序序列中,直到所有的记录插入完为止,得到一个新的有序序列
- 直接插入排序
插入排序思想:给已经有序的序列中插入新的数据
假设:第一个数据有序
插入过程:从有序序列的最后一个位置向前遍历,找到第一个小于待插入数据的位置,待插入元素之前的数据后移覆盖,待插入数据放在当前位置的下一个
void insert_sort(int* arr, int n)
{
int temp;
int j;
//1,找到待插入元素 从数组第二个元素开始
for (int i = 1; i < n; i++)
{
temp = arr[i];//2,temp存放插入元素
j = i - 1;//记录待插入元素的前一个元素位置
//3,待插入元素之前的数据比它大的后移覆盖
while (j >= 0 && arr[j] > temp)
{
arr[j + 1] = arr[j];
j--;
}
//4,空位(待插入位)赋值
arr[j + 1] = temp;//因为下一位置插入,所以j+1
}
}
**直接插入排序的特性总结:
- 元素集合越接近有序,直接插入排序算法的时间效率越高**
2. 时间复杂度:O(N^2)
3. 空间复杂度:O(1),它是一种稳定的排序算法
4. 稳定性:稳定 比如9 2 3 5 5 排序后 2 3 5 5 9 值相同元素相对位置没有改变
- 希尔排序
希尔排序为直接插入排序算法的优化
基本思想是:
先选定一个整数(步长step,也可以认为是分组间隔),把待排序文件中所有记录分成个组,所有距离为的记录分在同一组内,并对每一组内的记录进行排序。然后,取,重复上述分组和排序的工 作。当到达=1时,所有记录在统一组内排好序。
希尔排序:
(1)插入排序的优化
(2)步长:初始设置为 元素个数/2
(3)经过步长次排序:每次排完,步长减1
(4)每次排序都已步长为间隔给所有元素分组,组内插入排序(插入交换)
void shell_sort(int* arr, int n)
{
int step = n / 2;//初始化步长为n/2
int temp;
int j;
//1,找到待插入元素 从数组第二个元素开始
while (step >= 1)
{
for (int i = step; i < n; i++)
{
temp = arr[i];//2,temp保持待插入元素
j = i - step;//记录待插入元素的前步长个元素位置
//3,待插入元素之前的数据比它大的后移覆盖
while (j >= 0 && arr[j] > temp)
{
arr[j + step] = arr[j];
j -= step;
}
//4,空位(待插入位)赋值
arr[j + step] = temp;//因为j-step操作,所以j+step
}
step = step -1;
}
}
**希尔排序的特性总结:
- 希尔排序是对直接插入排序的优化。**
2. 当step > 1时都是预排序,目的是让数组更接近于有序。当step == 1时,数组已经接近有序的了,这样就会很快。这样整体而言,可以达到优化的效果。
3. 希尔排序的时间复杂度不好计算,需要进行推导,推导出来平均时间复杂度: O(N^1.3—N^2)
4. 稳定性:不稳定 分组后的交换会破坏相对位置关系
基本思想:
每一次从待排序的数据元素中选出最小(或最大)的一个元素,存放在序列的起始位置,直到全部待排序的 数据元素排完 。
- 直接选择排序
算法步骤:
1,在元素集合array[i]—array[n-1]中选择关键码最大(小)的数据元素
2,若它不是这组元素中的最后一个(第一个)元素,则将它与这组元素中的最后一个(第一个)元素交换
3,在剩余的array[i]—array[n-2](array[i+1]—array[n-1])集合中,重复上述步骤,直到集合剩余1个元素
我们在空间【0,n-1】选取最小值,与第一个元素交换,逐渐缩小空间,保证交换后头部的元素逐步有序
void select_sort(int* arr, int n)
{
//从未排序的序列中找最值,存放到未排序的起始位置
//未排序的区间[start,end]
int start = 0;
int end = n - 1;
while (start < end)
{
//找到最小值位置
int minidx = start;
for (int i = start + 1; i <= end; i++)
{
if (arr[i] < arr[minidx]) //if (arr[i] <= arr[minidx]):不稳定 相同元素 =导致更新,相对位置变化
minidx = i;
}
//把最小值位置存未排序开始的位置
int temp = arr[start];
arr[start] = arr[minidx];
arr[minidx] = temp;
//剩余未排序空间[start+1,end]
++start;
}
}
可以将上述过程进一步优化:减少遍历次数
每次从未排序区间找一个最大值和最小值 将最小值放在头部,最大值放在尾部
但需要特别注意:特殊处理 100 9 2 3 最大值就位于开始位置,找到最小值更新到开始位置后 2 9 100 3
由于最大值位置索引没有更新,再执行最大值放尾部 3 9 100 2 出现bug
需要判断最大值位置是否处于头部,如果处于头部,需要将最大值位置此时更新为原来最小值的位置
void select_sort2(int* arr, int n)
{
//从未排序的序列中找最值,存放到未排序的起始位置
//未排序的区间[start,end]
int start = 0;
int end = n - 1;
while (start < end)
{
//找到最小值位置
int minidx = start;
int maxidx = start;
for (int i = start + 1; i <= end; i++)
{
if (arr[i] < arr[minidx]) //if (arr[i] <= arr[minidx]):不稳定 相同元素 =导致更新,相对位置变化
minidx = i;
if (arr[i] > arr[maxidx])
maxidx = i;
}
//把最小值位置存未排序区间开始的位置
int temp = arr[start];
arr[start] = arr[minidx];
arr[minidx] = temp;
//特殊处理 100 9 2 3 最大值就位于开始位置,找到最小值更新到开始位置后 2 9 100 3
//由于最大值位置索引没有更新,再执行最大值放尾部 3 9 100 2 出现bug
//判断一下最大值位置是否位于起始位置
if (maxidx = start)
{
maxidx = minidx;//由于交换 此时最大值位置存放最小值,需要更新最大值位置为最小值位置,才可以找到最大值
}
//把最大值位置存未排序区间尾部的位置
int tmp = arr[end];
arr[end] = arr[maxidx];
arr[maxidx] = tmp;
//剩余未排序空间[start+1,end-1]
start++;
end--;
}
}
**直接选择排序的特性总结:
- 直接选择排序思考非常好理解,但是效率不是很好。实际中很少使用**
2. 时间复杂度:O(N^2)
3. 空间复杂度:O(1)
4. 稳定性:不稳定 产生位置交换
- 堆排序
队排序用大二叉树的知识,即完全二叉树实现大小堆
实现从小到大排序 ——->维护大堆
实现从大到小排序 ——->维护小堆
//向下调整
void shiftdown(int* arr, int n, int curpos)
{
int child = 2 * curpos + 1;
while (child < n)
{
if (child + 1 < n&&arr[child + 1] > arr[child])//找孩子中最大值
child++;
if (arr[child] > arr[curpos])
{
//大的值向上,当前值向下调整
int tmp = arr[curpos];
arr[curpos] = arr[child];
arr[child] = tmp;
//更新当前位置
curpos = child;
child = 2 * curpos + 1;
}
else
break;
}
}
void heap_sort(int* arr, int n)
{
//建大堆
for (int i = (n - 2) / 2; i >=0; i--)
{
shiftdown(arr,n,i);
}
//堆中最后一个元素
int end = n - 1;
while (end > 0)
{
//交换堆顶和最后一个元素
int temp = arr[0];
arr[0] = arr[end];
arr[end] = temp;
//除去最后一个元素 继续维护大堆
shiftdown(arr,end,0);
end--;
}
}
**直接选择排序的特性总结:
- 堆排序使用堆来选数,效率就高了很多。**
2. 时间复杂度:O(N*logN)
3. 空间复杂度:O(1)
4. 稳定性:不稳定 子节点与父节点交换
其中以交换排序中的冒泡排序和快速排序将经常出现在面试题中,本次博客重点分享这两种算法
交换排序的基本思想是:所谓交换,就是根据序列中两个记录键值的比较结果来对换这两个记录在序列中的位置,
交换排序的特点是:将键值较大的记录向序列的尾部移动,键值较小的记录向序列的前部移动。
- 冒泡排序
冒泡排序是大家比较熟悉的一种排序算法,这里不再做过多解释
#include<iostream>
using namespace std;
//冒泡排序 相邻元素比较,每一次遍历范围:0-未排序数据的最后一个位置
void bubble_sort(int* arr, int n)
{
for (int i = 0; i < n; i++)
{
int flag = 1;//标记一轮冒泡排序中是否发生交换操作
for (int j = 0; j < n - i - 1; j++)
{
if (arr[j] > arr[j + 1])
{
int temp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = temp;
flag = 0;
}
}
//如果没有发生交换,说明剩余元素全部有序 提前结束
if (flag == 1)
{
break;
}
}
}
测试代码如下:
#include<iostream>
using namespace std;
#include"sort.h"
void print(int* arr, int n)
{
for (int i = 0; i < n; i++)
{
cout << arr[i] << " ";
}
}
int main()
{
int arr[10] = { 9,66,54,33,5,78,123,0,7,12 };
//int arr[10] = { 1,3,5,7,9,11,13,15,17,19 };
//int arr[10] = { 1,13,15,17,19,0,2,4,6,8 };
cout << "before sort:" << endl;
print(arr, 10);
cout << endl;
cout << "after sort:" << endl;
bubble_sort(arr, 10);
print(arr, 10);
return 0;
}
测试结果如下:
冒泡排序的特性总结:
**1. 冒泡排序是一种非常容易理解的排序
- 时间复杂度:O(N^2)
- 空间复杂度:O(1)
稳定性:稳定**
- 快速排序
快速排序作为交换排序的一种算法,是许多家公司面试特别容易被问到的一个算法
首先我们要明白,快排为什么“快”?
这就要理解快排的算法思想,它是基于一种类似于二分法的划分,然后在通过划分结果分别再次划分,直至有序
在此过程中,我们可以采用递归的算法,也可以采用非递归的算法,后面我们将这两种实现方法都做一具体实现
划分的方法也各不相同,但目的都是一样的,根据基准值划分,保证在基准值一边大一边小
常见的划分方法有:
- hoare法:
算法步骤:
选取一个基准值
从剩余元素中开始遍历
1,从后往前找到第一个小于基准值的数据 保证一边大,一边小
2,从前往后找到第一个大于基准值的数据
3,交换两个位置的值
4,从交换的位置,分别开始,继续执行第一步
结束:从相遇位置的数据和基准值进行交换
int paration(int* arr, int begin, int end)
{
//选择基准值
int mid = getmid(arr, begin, end);//优化:三数取中 -> 划分均衡
// 取中作为基准值 交换两个位置的值
int tmp = arr[begin];
arr[begin] = arr[mid];
arr[mid] = tmp;
int key = arr[begin];
int start = begin;
while (begin < end)
{
//从后往前找到第一个小于基准值的数据
while (begin < end&&arr[end] >= key)
{
end--;
}
//从前往后找到第一个大于基准值的数据
while (begin < end&&arr[begin] <= key)
{
begin++;
}
//交换两个位置的值
int tmp = arr[end];
arr[end] = arr[begin];
arr[begin] = tmp;
}
//从相遇位置的数据和基准值进行交换
int temp = arr[begin];
arr[begin] = arr[start];
arr[start] = temp;
return begin;//返回划分位置 保证一边大一边小
}
在实际测试过程中,发现针对有序的数据采用这种划分方法,在递归调用的过程中,递归调用次数取决于数据量的大小,数据量过大将会导致,栈溢出
对此,本代码进行优化,具体如何优化呢?
首先分析,为什么有序的数据划分出现了问题,这是因为划分不均衡,导致数据划分在集中一侧
为了保证划分均衡,对于基准值的选择进行了优化
采用:三数取中,分别以一组数据的开头,中间,结尾,以其三个数的中间值作为基准值,无论一组数据是否有序,都可以使得划分均衡
//优化:三数取中 -> 划分均衡
int getmid(int* arr, int begin, int end)
{
int mid = (end - begin) / 2;
if (arr[begin] < arr[end])
{
if (arr[begin] > arr[mid])
return begin;
else if (arr[mid] > arr[end])
return end;
else
return mid;
}
else
{
if (arr[end] > arr[mid])
return end;
else if (arr[mid] > arr[begin])
return begin;
else
return mid;
}
}
三数取中,返回三个数的中间值作为基准值,从而后续进行划分
在划分过程中需要注意:
如果是递增排序:
则: 1,从后往前找到第一个小于基准值的数据 保证一边大,一边小
2,从前往后找到第一个大于基准值的数据
如果递减排序:
则: 1,从后往前找到第一个大于基准值的数据 保证一边大,一边小
2,从前往后找到第一个小于基准值的数据
注意到,二者都是先从后往前,然后再 从前往后,如果颠倒将无法保证划分结果任然是一边大,一边小
- 挖坑法:
算法步骤:
选取一个基准值,保存基准值
从剩余元素中开始遍历
1,从后往前找到第一个小于基准值的数据 ,将数据挖出填入begin位置的坑里
2,从前往后找到第一个大于基准值的数据,将其填入上一步挖出所产生的的坑里
结束:从相遇位置的数据将基准值进行填入
//挖坑法 划分
int paration2(int* arr, int begin, int end)
{
//选择基准值
int key = arr[begin];
while (begin < end)
{
//从后往前找到第一个小于基准值的数据
while (begin < end&&arr[end] >= key)
{
end--;
}
arr[begin] = arr[end];
//从前往后找到第一个大于基准值的数据
while (begin < end&&arr[begin] <= key)
{
begin++;
}
arr[end] = arr[begin];
}
//从相遇位置的数据将基准值进行填入
arr[end] = key;
return begin;//返回划分位置 保证一边大一边小
}
- 前后指针法
算法步骤
prev:上一个小于基准值的位置
cur: 下一个小于基准值的位置
当cur走到一个小于基准值的位置,则:
判断prev和cur是否连续:
如果连续:区间[prev,cur]的值都是不大于基准值,更新prev, cur
如果不连续:区间[prev, cur]的值,有大于基准值的,更新prev,数据交换,更新cur
//前后指针法 划分
int paration3(int* arr, int begin, int end)
{
//选择基准值
int key = arr[begin];
int prev = begin;//上一个小于基准值的位置
int cur = begin + 1;//下一个小于基准值的位置
while (cur <= end)
{
if (arr[cur] < key&&++prev != cur)//不连续
{
//交换两个位置的值
int tmp = arr[prev];
arr[prev] = arr[cur];
arr[cur] = tmp;
}
cur++;
}
//交换两个位置的值
int tmp = arr[begin];
arr[begin] = arr[prev];
arr[prev] = tmp;
return prev;
}
得到划分结果后,进行快排,包括递归和非递归两种形式:
递归快排:(二分法思想)
//递归法
void quicksort(int* arr,int begin,int end)
{if (begin >= end)
return;
int div = paration3(arr, begin, end);
quicksort(arr,begin,div-1);
quicksort(arr, div + 1, end);
}
非递归快排:
分别借助,顺序表尾插,尾删的操作,以及队列尾进头出的数据结构完成非递归快排
主要通过借助数据结构保存划分结束后的分组情况
类似情况如下图:
分组结果中,数据个数不超过两个的,单独一组,不在进行分组存储,如果超过两个的分组存储在数据结构中
左右两个边界,left和right
如果是顺序表,尾插操作,先插入right,再插入left,获取的时候,尾删操作,先获取尾部最后一个元素left,尾删后在获取最后一个元素right,保证先左后右的顺序,直至顺序表为空结束。
//非递归法
void quicksort2(int* arr, int n)
{
//创建一个顺序表,保持划分的区间
SeqList sl;
SeqListInit(&sl);
//首先保存整个空间
// 尾插:先放右,在放左 【0,n-1】
SeqListPushBack(&sl, n - 1);
SeqListPushBack(&sl, 0);
int left, right;
while (!SeqListisEmpty(&sl))
{
//从尾部取出尾删 实现取出先左后右
left = SeqListBack(&sl);
SeqListPopBack(&sl);
right = SeqListBack(&sl);
SeqListPopBack(&sl);
int div = paration(arr, left, right);
if (left < div - 1)//区间长度超过2个元素
{
SeqListPushBack(&sl, div - 1);
SeqListPushBack(&sl, left);
}
if (div + 1 < right)
{
SeqListPushBack(&sl, right);
SeqListPushBack(&sl, div + 1);
}
}
}
对于此,可以考虑使用队列先进先出的结构来实现
左边界left先进,最后左边界left先出
//非递归法 队列
void quicksort3(int* arr, int n)
{
//创建一个队列,保持划分的区间
Queue q;
initQueue(&q);
//首先保存整个空间
// 队尾插入先左后右 【0,n-1】
queuePush(&q, 0);
queuePush(&q, n-1);
int left, right;
while (queueSize(&q)>0)
{
//从队头出
left = queueFront(&q);
queuePop(&q);
right = queueFront(&q);
queuePop(&q);
//划分 产生新的左右区间
int div = paration(arr, left, right);
if (left < div - 1)//区间长度超过2个元素,需要保存到队列
{
queuePush(&q, left);
queuePush(&q, div - 1);
}
if (div + 1 < right)
{
queuePush(&q, div + 1);
queuePush(&q, right);
}
}
}
非递归即为数组划分结果的存储,直至划分结果为单独数据时即为有序
快速排序的特性总结:
**1. 快速排序整体的综合性能和使用场景都是比较好的,所以才敢叫快速排序
- 递归式
首先根据合并两个有序序列的思想(合并两个有序链表)实现合并的代码,但合并的前提就是,两个序列必须有序
//合并前提 两个数组有序
void merge(int* arr,int left,int right)
{
int mid = left+(right - left) / 2;
int len = right - left + 1;
//创建临时数组
int* temp = (int*)malloc(sizeof(int)*len);
int k = 0; //临时数组下标
int p = left; //左数组首元素下标
int q = mid + 1;//右数组首元素下标
//先有一半存入临时数组
while (p <= mid && q <= right)
{
if (arr[p] < arr[q])
{
temp[k++] = arr[p++];//谁小谁先放入临时数组中
}
else
{
temp[k++] = arr[q++];
}
}
//剩余元素排最后
while (p <= mid)
{
temp[k++] = arr[p++];
}
while (q <= right)
{
temp[k++] = arr[q++];
}
//拷贝回原数组
memcpy(arr+left,temp,sizeof(int)*len);
free(temp);
temp = NULL;
}
在合并过程中,我们动态开辟空间进行临时合并数据存储,合并完成后拷贝回原数组,需要注意到,拷贝回原数组,原始数组的首地址需要变化,以左边界left作为偏移量,在后续递归过程中就可以看到为什么要有偏移量的操作了
针对需要排序的数组,首先通过递归调用的方式将其进行区间划分,直至区间仅剩单独元素停止继续划分
//针对无序数组 先拆分直至有序 然后依次合并
void Mergesort(int* arr, int l, int r)
{
if (l == r) //区间只有一个元素,无需排序
{
return;
}
int m = l + (r - l) / 2;
Mergesort(arr, l, m); //拆分左数组
Mergesort(arr, m+1, r); //拆分右数组
merge(arr, l, r);//合并
}
- 非递归式
与递归式相同,首先写出合并代码,为了方便后续非递归的实现,此时的中间索引mid,不在通过左右边界的关系来产生,而是作为参数传入
//合并前提 两个数组有序
void merge2(int* arr, int left,int mid, int right)
{
int len = right - left + 1;
//创建临时数组
int* temp = (int*)malloc(sizeof(int)*len);
int k = 0; //临时数组下标
int p = left; //左数组首元素下标
int q = mid + 1;//右数组首元素下标
//先有一半存入临时数组
while (p <= mid && q <= right)
{
if (arr[p] < arr[q])
{
temp[k++] = arr[p++];//谁小谁先放入临时数组中
}
else
{
temp[k++] = arr[q++];
}
}
//剩余元素排最后
while (p <= mid)
{
temp[k++] = arr[p++];
}
while (q <= right)
{
temp[k++] = arr[q++];
}
//拷贝回原数组
memcpy(arr + left, temp, sizeof(int)*len);
free(temp);
temp = NULL;
}
在进行非递归划分时,观察下图,根据拆分及合并顺序,推导得出左右边界的索引关系
//非递归 归并排序
void Mergesort2(int* arr, int n)
{
int step = 1;
while (step < n)
{
for (int idx = 0; idx < n; idx += 2 * step)
{
//找到两个待合并子区间
int begin = idx;
int mid = idx + step - 1;//第一个子区间结束位置
//判断是否存在第二个子区间
if (mid >= n - 1)
{
continue;//不存在第二个子区间,直接跳过
}
int end = idx + 2 * step - 1;
//判断第二个子序列是否越界
if (end >= n)
{
end = n - 1;
}
merge2(arr,begin,mid,end);
}
step *= 2;
}
}
**归并排序的特性总结:
- 归并的缺点在于需要O(N)的空间复杂度,归并排序的思考更多的是解决在磁盘中的外排序问题。**
2. 时间复杂度:O(N*logN)
3. 空间复杂度:O(N)
4. 稳定性:稳定
还没有评论,来说两句吧...