文章目录
写在前面
博主根据十大必学经典排序算法这篇博文进行了学习,并把自己遇到的困难和所写的代码进行记录,本文按照如下顺序进行讲解(其中我把冒泡放在了插入排序之前)
00复杂度计算
01选择排序
思想
选择排序就是不断地从未排序的元素中选择最大(或最小)的元素放入已排好序的元素集合中,直到未排序中仅剩一个元素为止
如何选出最小元素
先随便选一个元素假设它为最小的元素(默认为无序区间第一个元素),然后让这个元素与无序区间中的每一个元素进行比较,如果遇到比自己小的元素,那更新最小值下标,直到把无序区间遍历完,那最后的最小值就是这个无序区间的最小值
代码
#include<iostream>
using namespace std;
void selectSort(int arr[], int length);
int main()
{
int arr[8] = { 6,5,3,1,8,7,2,4 };
selectSort(arr, 8);
for (int i = 0; i < 8; i++)
{
cout << arr[i] << " ";
}
return 0;
}
void selectSort(int arr[],int length)
{
//选择length-1次
for (int i = 0; i < length-1; i++)
{
int minIndex = i;//设i为最小元素的下标
//在除前i个元素之外的外围内选择一个最小的元素
for (int j = i+1; j <length; j++)
{
if (arr[minIndex]>arr[j])
{
//记录最小元素的下标
minIndex = j;
}
}
//交换
int temp = arr[i];
arr[i] = arr[minIndex];
arr[minIndex] = temp;
}
}
时间复杂度
O(n^2)
稳定性
由于选择元素之后会发生交换操作,所以有可能把前面的元素交换到后面,所以不是稳定的排序
参考资料
02冒泡排序
思想
从第一个数开始,让它和右边相邻的数进行比较,如果左边的数大于右边的石子,那么就交换两个数的位置,(也可以左小于右交换,这里采用大于交换),这样每比较一次,大的就跑到右边,直到跑到最右边。
代码
#include<iostream>
using namespace std;
void bubbleSort(int arr[], int len);
int main() {
int arr[8] = { 6,5,3,1,8,7,2,4 };
bubbleSort(arr, 8);
for (int i = 0; i < 8; i++)
{
cout << arr[i] << " ";
}
return 0;
}
void bubbleSort(int arr[], int len) {
//比较的趟数
for (int i = 0; i < len-1; i++)
{
//第i趟比较的次数
for (int j = 0; j < len-i-1; j++)
{
//交换
if (arr[j]>arr[j+1])
{
int temp = arr[j+1];//存储第j+1个元素
arr[j+1] = arr[j];//将j个元素赋值给第j+1个元素
arr[j] = temp;//将第j+1个元素赋值给第j个元素
}
}
}
}
时间复杂度
O(n^2)
稳定性
所谓稳定性,其实就是说,当你原来待排的元素中间有相同的元素,在没有排序之前它们之间有先后顺序,在排完后它们之间的先后顺序不变,我们就称这个算法是稳定的
冒泡排序就是一个稳定的排序了,因为在交换的时候,如果两个数相同,那么就不交换[if (arr[j] > arr[j+1]){ 交换}],相同元素不会因为算法中哪条语句而相互交换位置的。
参考资料
03插入排序
思想
所谓直接插入排序,就是把未排序的元素一个一个地插入到有序的集合中,插入时就像你那样,把有序集合从后向前扫一遍,找到合适的位置插入
适用场景
- 插入排序在对几乎已经排好序的数据操作时,效率高,即可以达到线性排序的效率;
- 但插入排序一般来说是低效的,因为插入排序每次只能将数据移动一位;
- 小规模数据或者基本有序时效率高
代码
#include<iostream>
#include<stdio.h>
using namespace std;
void insert_sort(int arr[], int length);
int main()
{
int arr[8] = { 6,5,3,1,8,7,2,4 };
insert_sort(arr, 8);
for (int i = 0; i <8; i++)
{
cout << arr[i] << " ";
}
return 0;
}
void insert_sort(int arr[], int length)
{
int i,j;//j为要插入的位置
for (i = 1; i < length; i++)
{
int temp = arr[i];//保留当前元素
//寻找要插入的位置
for (j = i; j > 0 && arr[j - 1] > temp; j--)
{
arr[j] = arr[j - 1];
}
//插入到找到的位置
arr[j] = temp;
}
}
时间复杂度
时间复杂度O(n^2)
稳定性
插入排序是稳定的排序算法,因为在比较的时候,如果两个数相等的话,不会进行移动,前后两个数的次序不会发生改变
参考资料
04希尔排序
希尔排序,也称递减增量排序算法,是插入排序的一种更高效的改进版本。但希尔排序是非稳定排序算法。
思想
首先它把较大的数据集合分割成若干个小组(逻辑上分组),然后对每一个小组分别进行插入排序,此时,插入排序所作用的数据量比较小(每一个小组),插入的效率比较高
适用场景
希尔排序是基于插入排序的以下两点性质而提出改进方法的:
插入排序在对几乎已经排好序的数据操作时,效率高,即可以达到线性排序的效率;但插入排序一般来说是低效的,因为插入排序每次只能将数据移动一位;
因此插入排序适用于数据规模较大并且无序的场景
代码
#include<iostream>
using namespace std;
void ShellSort(int arr[], int len);
int main() {
int arr[8] = { 6,5,3,1,8,7,2,4 };
ShellSort(arr, 8);
for (int i = 0; i < 8; i++)
{
cout << arr[i] << " ";
}
return 0;
}
void ShellSort(int arr[], int len)
{
//设置增量
for (int gap = len/2; gap>0 ; gap/=2)
{
//对每一个根据增量获得的分组进行插入排序
int i, j;
for (i = gap; i <len; i++)
{
int temp = arr[i];
for (j = i; j>0 && arr[j-gap]>temp; j-=gap)
{
arr[j] = arr[j - gap];
}
arr[j] = temp;
}
}
}
希尔排序只是在外层多加了一个控制变量gap的循环罢了
时间复杂度
希尔排序的复杂度和增量序列是相关的
{1,2,4,8,…}这种序列并不是很好的增量序列,使用这个增量序列的时间复杂度(最坏情形)是O(n^2)
Hibbard提出了另一个增量序列{1,3,7,…,2^k-1},这种序列的时间复杂度(最坏情形)为 O(n^1.5)
Sedgewick提出了几种增量序列,其最坏情形运行时间为O(n^1.3),其中最好的一个序列是{1,5,19,41,109,…}
稳定性
不是稳定的,虽然插入排序是稳定的,但是希尔排序在插入的时候是跳跃性插入的,有可能徘徊稳定性
参考资料
05归并排序
归并排序2020.8.18更新
代码
//归并排序
#include<iostream>
#include<vector>
using namespace std;
class Solution {
public:
vector<int> sortArray(vector<int>& nums) {
vector<int> temp(nums.size());
merge_sort(nums, temp, 0, nums.size()-1);
return nums;
}
void merge_sort(vector<int>& arr, vector<int>& temp, int left, int right) {
//left==right的时候,就递归到只有一个元素-->终止条件
if (left < right) {
//分:将数组一分为二
int center = left + (right - left) / 2;
//治:将左边的数组排序,left->center
merge_sort(arr, temp, left, center);
//治:将右边的数组排序,center+1->right
merge_sort(arr, temp, center + 1, right);
//合:合并两个有序数组
merge(arr, temp, left, center, right);
}
}
void merge(vector<int>& arr, vector<int>& temp, int left, int center, int right) {
int i = left, j = center + 1;
//先通过比较将两个有序数组合并为一个有序数组,结果暂时放到temp数组中
for (int k = left; k <= right; k++) {//从小到大排序
//如果左边数组arr[left...center]中的元素取完[即比较完](i>center),
//则直接copy右边数组的元素到辅助数组,右边数组同理
if (i > center) { temp[k] = arr[j++]; }
else if (j > right) { temp[k] = arr[i++]; }
else if (arr[i] < arr[j]) { temp[k] = arr[i++]; }
else { temp[k] = arr[j++]; }
}
//再将已经排好序的辅助数组中的值复制到原数组arr中
for (int k = left; k <= right; k++) {
arr[k] = temp[k];
}
}
};
class Solution {
public:
vector<int> sortArray(vector<int>& nums) {
vector<int> temp(nums.size());
merge_sort(nums, temp, 0, nums.size()-1);
return nums;
}
void merge_sort(vector<int>& arr, vector<int>& temp, int left, int right) {
//left==right的时候,就递归到只有一个元素-->终止条件
if (left < right) {
//分:将数组一分为二
int center = left + (right - left) / 2;
//治:将左边的数组排序,left->center
merge_sort(arr, temp, left, center);
//治:将右边的数组排序,center+1->right
merge_sort(arr, temp, center + 1, right);
//合:合并两个有序数组
int i = left, j = center + 1;
//先通过比较将两个有序数组合并为一个有序数组,结果暂时放到temp数组中
for (int k = left; k <= right; k++) {//从小到大排序
//如果左边数组arr[left...center]中的元素取完[即比较完](i>center),
//则直接copy右边数组的元素到辅助数组,右边数组同理
if (i > center) { temp[k] = arr[j++]; }
else if (j > right) { temp[k] = arr[i++]; }
else if (arr[i] < arr[j]) { temp[k] = arr[i++]; }
else { temp[k] = arr[j++]; }
}
//再将已经排好序的辅助数组中的值复制到原数组arr中
for (int k = left; k <= right; k++) {
arr[k] = temp[k];
}
}
}
};
思想
所谓归并排序,就是将待排序的数分成两半后排好序,然后再将两个
排好序的序列合并成一个有序序列
适用场景
归并排序是建立在归并操作的一种高效的排序方法,该方法采用了分治的思想,比较适用于处理较大规模的数据,但比较耗内存
#include<iostream>
using namespace std;
int temp[8];
void MergeSort(int arr[], int temp[], int left, int right);
void Merge(int arr[], int temp[], int left, int center, int right);
int main()
{
int arr[8] = { 6,5,3,1,8,7,2,4 };
MergeSort(arr, temp,0,7);
for (int i = 0; i < 8; i++)
{
cout << arr[i] << " ";
}
return 0;
}
void MergeSort(int arr[],int temp[],int left,int right) {
//递归边界
if (left>=right)
{
return;
}
int center = (left + right) / 2;
//分治
//分治左边
MergeSort(arr, temp, left, center);
//分治右边
MergeSort(arr, temp, center + 1, right);
//合并
Merge(arr, temp, left, center, right);
}
void Merge(int arr[],int temp[],int left,int center,int right)
{
int i=left, j=center+1;
//合并
for (int k=left; k<=right; k++)
{
//如果center左侧取完,则直接copy右边数组到辅助数组
if (i>center)
{
temp[k] = arr[j++];//自己写错了
}
//如果center右侧取完,则直接copy左边数组到辅助数组
else if (j > right)
{
temp[k] = arr[i++];//自己写错了
}
//如果右侧的元素大于左侧的元素,则将左侧元素赋值到temp中
else if (arr[i] < arr[j]) {
temp[k] = arr[i++];
}
//如果右侧的元素小于左侧的元素,则将右侧元素赋值到temp中
else
{
temp[k] = arr[j++];
}
}
//再将临时数组中的元素赋值到arr数组中
for (int i = left; i <= right; i++)
{
arr[i] = temp[i];
}
}
时间复杂度
复杂度为O(NlogN)计算,详细计算见参考文章
稳定性
是稳定的,因为在合并的时候,如果相等,选择前面的元素到辅助数组
参考文章
归并排序的非递归版本
博主待学习
06快速排序
思想
快速排序也是和归并排序差不多,基于分治的思想以及采取递归的方式来处理子问题。例如对于一个待排序的源数组arr = { 4,1,3,2,7,6,8}。
我们可以随便选一个元素,假如我们选数组的第一个元素吧,我们把这个元素称之为”主元“吧。
然后将大于或等于主元的元素放在右边,把小于或等于主元的元素放在左边。
通过这种规则的调整之后,左边的元素都小于或等于主元,右边的元素都大于或等于主元,很显然,此时主元所处的位置,是一个有序的位置,即主元已经处于排好序的位置了。
主元把数组分成了两半部分。把一个大的数组通过主元分割成两小部分的这个操作,我们也称之为分割操作(partition)
接下来,我们通过递归的方式,对左右两部分采取同样的方式,每次选取一个主元 元素,使他处于有序的位置。
那什么时候递归结束呢?当然是递归到子数组只有一个元素或者0个元素了
使用场景
不需要像归并排序那样,还需要一个临时的数组来辅助排序,这可以节省掉一些空间的消耗,而且他不像归并排序那样,把两部分有序子数组汇总到临时数组之后,还得在复制回源数组,因此适用于对空间消耗有要求的排序
分割操作
分割操作:单向调整
代码——单向调整
#include<iostream>
using namespace std;
void QuickSort(int a[], int left, int right);
int partion(int a[], int left, int right);
int main()
{
int a[7] = { 8,1,3,2,7,6,4 };
QuickSort(a, 0, 6);
for (int i = 0; i < 7; i++)
{
cout << a[i] << "";
}
return 0;
}
int partion(int a[], int left, int right)
{
int temp, pivot;//pivot存放主元
int i, j;
i = left;
pivot = a[right];//选取最右边元素为主元
for (j = left; j < right; j++)
{
//如果第j个元素小于主元,则第j个元素与第i个元素进行交换
if (a[j] < pivot) {
temp = a[j];
a[j] = a[i];
a[i] = temp;
i++;//i向前移动一位
}
}
//在下标i的左边,元素均小于主元
//将主元与第i个元素进行交换
a[right] = a[i];
a[i] = pivot;
//返回最终的主元下标
return i;
}
void QuickSort(int a[], int left, int right)
{
if (left < right)
{
//求出每次快排分割的边界
int center = partion(a, left, right);
QuickSort(a, left, center-1);
QuickSort(a, center+1, right);
}
}
分割操作:双向调整
i 向右遍历的过程中,如果遇到大于或等于主元的元素时,则停止移动,j向左遍历的过程中,如果遇到小于或等于主元的元素则停止移动。
代码——双向调整
//双向调整的快速排序
#include<iostream>
using namespace std;
void QuickSort(int a[], int left, int right);
int partion2(int a[], int left, int right);
int main()
{
int a[7] = { 8,1,3,2,7,6,4 };
QuickSort(a, 0, 6);
for (int i = 0; i <=6; i++)
{
cout << a[i] << " " << endl;
}
return 0;
}
void QuickSort(int a[], int left, int right)
{
if (left < right)
{
int center = partion2(a, left, right);
QuickSort(a, left, center - 1);
QuickSort(a, center + 1, right);
}
}
int partion2(int a[], int left, int right)
{
int i=left+1, j=right, temp;
int pivot = a[left];//主元
while (true)
{
//向右遍历扫描
while (i <= j && a[i] <= pivot) i++;
//向左遍历扫描
while (i <= j && a[j] >= pivot) j--;
if (i >= j) break;
//交换
temp = a[i];
a[i] = a[j];
a[j] = temp;
}
//把a[j]与主元交换
a[left] = a[j];
a[j] = pivot;
return i;
}
时间复杂度
快速排序的最坏时间复杂度是O(n^2).
快速排序的平均时间复杂度是O(nlogn).
例如有可能会出现一种极端的情况,每次分割的时候,主元左边的元素个数都为0,而右边都为n-1个。这个时候,就需要分割n次了。而每次分割整理的时间复杂度为O(n),所以最坏的时间复杂度为O(n^2)。
而最好的情况就是每次分割都能够从数组的中间分割了,这样分割logn次就行了,此时的时间复杂度为O(nlogn)。
而平均时间复杂度,则是假设每次主元等概率着落在数组的任意位置,最后算出来的时间复杂度为O(nlogn),至于具体的计算过程,我就不展开了。
不过显然,像那种极端的情况是极少发生的。
稳定性
不稳定排序算法
参考资料
快速排序具体讲解见参考优质文章