面试常被问到排序算法总结(插入+选择+交换+归并排序)
排序算法总结
谈及排序算法,大家知道常见的排序算法都有哪些呢?
下面来总结一下常见的排序算法:
插入排序
基本思想:
把待排序的记录按其关键码值的大小逐个插入到一个已经排好序的有序序列中,直到所有的记录插入完为止,得到一个新的有序序列
直接插入排序
插入排序思想:给已经有序的序列中插入新的数据
假设:第一个数据有序
插入过程:从有序序列的最后一个位置向前遍历,找到第一个小于待插入数据的位置,待插入元素之前的数据后移覆盖,待插入数据放在当前位置的下一个
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}
}
直接插入排序的特性总结:
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/2int 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;}
}
希尔排序的特性总结:
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--;}
}
直接选择排序的特性总结:
1. 直接选择排序思考非常好理解,但是效率不是很好。实际中很少使用
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;}elsebreak;}
}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--;}
}
直接选择排序的特性总结:
1. 堆排序使用堆来选数,效率就高了很多。
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. 冒泡排序是一种非常容易理解的排序
2. 时间复杂度:O(N^2)
3. 空间复杂度:O(1)
4. 稳定性:稳定
快速排序
快速排序作为交换排序的一种算法,是许多家公司面试特别容易被问到的一个算法
首先我们要明白,快排为什么“快”?
这就要理解快排的算法思想,它是基于一种类似于二分法的划分,然后在通过划分结果分别再次划分,直至有序
在此过程中,我们可以采用递归的算法,也可以采用非递归的算法,后面我们将这两种实现方法都做一具体实现
划分的方法也各不相同,但目的都是一样的,根据基准值划分,保证在基准值一边大一边小
常见的划分方法有:
- 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;elsereturn mid;}else{if (arr[end] > arr[mid])return end;else if (arr[mid] > arr[begin])return begin;elsereturn 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. 快速排序整体的综合性能和使用场景都是比较好的,所以才敢叫快速排序
2. 时间复杂度:O(N*logN)
3. 空间复杂度:O(logN)
4. 稳定性:不稳定
归并排序
递归式
首先根据合并两个有序序列的思想(合并两个有序链表)实现合并的代码,但合并的前提就是,两个序列必须有序
//合并前提 两个数组有序
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;}
}
归并排序的特性总结:
1. 归并的缺点在于需要O(N)的空间复杂度,归并排序的思考更多的是解决在磁盘中的外排序问题。
2. 时间复杂度:O(N*logN)
3. 空间复杂度:O(N)
4. 稳定性:稳定
面试常被问到排序算法总结(插入+选择+交换+归并排序)相关推荐
- 面试中的 10 大排序算法总结
前言 查找和排序算法是算法的入门知识,其经典思想可以用于很多算法当中.因为其实现代码较短,应用较常见.所以在面试中经常会问到排序算法及其相关的问题.但万变不离其宗,只要熟悉了思想,灵活运用也不是难事. ...
- 面试时写不出排序算法?看这篇就够了
点击上方"方志朋",选择"设为星标" 做积极的人,而不是积极废人 作者:静默虚空 juejin.im/post/5cb6b8f551882532c334bcf2 ...
- Java常见排序算法之直接选择排序
在学习算法的过程中,我们难免会接触很多和排序相关的算法.总而言之,对于任何编程人员来说,基本的排序算法是必须要掌握的. 从今天开始,我们将要进行基本的排序算法的讲解.Are you ready?Let ...
- 排序算法:简单选择排序算法实现及分析
简单选择排序算法介绍 简单选择排序(Simple Selection Sort)就是通过n-1次关键字排序之间的比较,从n-i+1个记录中选择关键字最小的记录,并和第i(1<=i<=n)记 ...
- java选择排序代码_Java排序算法总结之选择排序
本文实例讲述了Java排序算法总结之选择排序.分享给大家供大家参考.具体分析如下: 选择排序的基本操作就是每一趟从待排序的数据元素中选出最小(或最大)的一个元素,顺序放在已排好序的数列的最后,直到全部 ...
- Java排序算法之直接选择排序
Java排序算法之直接选择排序 基本过程:假设一序列为R[0]~R[n-1],第一次用R[0]和R[1]~R[n-1]相比较,若小于R[0],则交换至R[0]位置上.第二次从R[1]~R[n-1]中选 ...
- js排序算法详解-选择排序
全栈工程师开发手册 (作者:栾鹏) js系列教程5-数据结构和算法全解 js排序算法详解-选择排序 相对于冒泡排序还有一种类似的方法就是选择排序,顾名思义就是选择性排序,什么意思呢? 这么来理解,假设 ...
- 《数据结构与算法》实验:排序算法实验比较——选择排序 堆排序
<数据结构与算法>实验和课程Github资源 <数据结构与算法>实验:线性结构及其应用--算术表达式求值 <数据结构与算法>实验:树型结构的建立与遍历 <数据 ...
- 排序算法六:选择排序之直接选择排序
排序算法六:选择排序之直接选择排序 声明:引用请注明出处http://blog.csdn.net/lg1259156776/ 引言 在我的博文<"主宰世界"的10种算法短评& ...
最新文章
- np301串口服务器修改ip,NP301 1路RS-232/485/422 TCP/IP 串口服务器
- Spring学习笔记十七---事务的转播行为
- 【Java基础】字符串与数组
- 信息学奥赛一本通(C++)在线评测系统——基础(一)C++语言—— 1058:求一元二次方程
- 【渝粤题库】陕西师范大学164111 Java及JSP动态网页编程与应用 作业 (高起专)
- golang 没有名字参数_Go 返回参数命名
- Vue与Element入门使用
- debian uefi legacy 区别_电脑硬盘格式有Legacy与UEFI,选择哪一个好?千万别选错了!...
- Maven 3.0 RC1 发布
- asp.net中使用下拉菜单的级联问题
- 使用pillow简易创建个图形验证码
- Linux mmc驱动框架(4)——卡检测及初始化
- 为地图marker 设置网络图片
- Python爬取某站上海租房图片!
- 推荐28个网站,让你边玩边学
- C++简单程序典型案例
- 如何使用Python进行异常检测?我来教你呀~
- 6.5Linux-rpm和tar
- 什么是 JavaScript 引擎
- 图片轮换的按钮如何通过像素定位
热门文章
- CASS11.0新功能介绍(支持AutoCAD2020平台,用户可以体验高雅黑带来的极致眼颜)
- CPCL/TSPL指令查看仿真
- 各个地区2.4G及5G信道
- css(小程序)强制不换行
- mac忘记mysql用户名和密码_Mac 最全 Mysql忘记用户名和密码如何处理
- 6个经典Python项目让你快速具备独立开发能力
- socket文件传输服务器,Socket文件传输(含服务端以及客户端源码)
- 股票费用计算机,【股票买卖收益的计算器】股票买卖费用怎么算?_千聊
- ECLIPSE 安装及与CDT 的使用 多线程编程
- 从零开始机器学习-03