蓝色代码膜拜大佬https://mp.weixin.qq.com/s/iiH2wSG-hVeUHxIsfuu9gw
1. 冒泡排序
遍历若干次要排序的序列,每次遍历时,都从前往后依次比较相邻的两个数的大小,如果顺序错误就交换位置。这样,一次遍历以后,最大(or最小)的元素就在数列的末尾。采用相同的方法再次遍历,第二大(or小)的元素就被排在最大元素之前。重复此操作,直到整个数列都有序为止。
C++实现:
void bubble(int arr[], int size) //参数为数组和数组大小
{
for (int i = 0; i < size - 1; i++) //注意跳出条件为size-1:
//后面的比较是当前值和后一个值比较,所以不能取到最后一个数!
{
for (int j = 0; j < size - 1 - i; j++) //每次选出最大的只有,最大的不进行排序,所以长度减少1
{
if (arr[j] > arr[j + 1])
{
int temp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = temp;
}
}
}
}
- 冒泡排序的复杂度和稳定性1:
- 冒泡排序的时间复杂度是O(N^2),空间复杂度为O(1);
- 冒泡排序是稳定的算法。
冒泡优化
冒泡有个最大的问题是不管有序还是没序,都直接进行循环。因此可以提出改进:
设定一个临时遍历来标记该数组是否有序,如果有序了就不用遍历了。
void bubblie_advance(int *arr,int size)
{
int flag; //标记
for(int i=size-1; i>0; i--)
{
flag = 0; //flag初始化为0
for(int j=0; j<i; j++)
{
if(arr[j] > arr[j+1])
{
int temp=arr[j];
arr[j]=arr[j+1];
arr[j+1]=temp;
//若发生交换,则设标记为1
flag = 1;
}
}
if(flag==0) //在里层循环的外面进行查flag值,目的是让循环先进行一次
break; //若没有发生交换,说明数列已有序
}
}
2. 选择排序
首先找到数组中最小元素,将它和数组的第一个元素交换位置;在剩下的元素中继续寻找最小的元素,和数组的第二个元素交换位置。如此循环,直至整个数组排序完成。
C++实现:
void SelectSort(int *arr, int size)
{
if (arr == NULL)
return;
//1.找到无序区中最小的元素和它的下标
int min_index;
for (int i = 0; i < size ; i++)
{
min_index = i;
for (int j = i + 1; j < size; j++)
{
if (arr[j] < arr[min_index])
min_index = j;
}
//2.把最小的元素与无序区第一个元素交换
if (min_index != i)
{
//swap(arr[i], arr[min_index]);
int tmp = arr[i];
arr[i] = arr[min_index];
arr[min_index] = tmp;
}
}
}
- 选择排序的复杂度和稳定性:
- 时间复杂度为O(N^2),空间复杂度为O(1);
- 稳定性为不稳定。
选择排序优化:
每趟排序同时找到最大值和最小值,把最小值放在左边,最大值放在右边,然后同时缩小左右排序范围。
void SelectSort_advanced(int *arr,int size)
{
if(arr == NULL)
return;
int left = 0;
int right = size-1;
while(left < right)
{
for(int i=left; i<right; i++)
{
if(arr[i] < arr[left])
swap(arr[i], arr[left]);
if(arr[i] > arr[right])
swap(arr[i],arr[right]);
}
left++;
right--;
}
}
3. 插入排序
把n个待排序的元素看成是一个有序表和一个无序表,开始时有序表中只有一个元素,无序表中有n-1个元素;排序过程每次从无序表中取出第一个元素,将它插入到有序表大小的对应位置处,重复n-1次完成整个排序过程。
void insertSort(int *arr, int size)
{
for(int i=1; i<size; i++) //外部循环为依次从无序表中取数
{
int value = arr[i]; //必不可少的!!!
int j; //插入位置
for(j=i-1; j>=0; j--) //内部循环为向有序表中插数
{
//有序表中本身都是排好序的,将无序表中取出的数从后往前比较,如果大于有序表中最后一个数,
//说明它大于有序表中所有数,直接break即可。
if(arr[j] > value)
arr[j+1] = arr[j]; //移动数据
else
break;
}
arr[j+1] = value; //插入数据
}
}
- 插入排序复杂度和稳定性:
- 时间复杂度为O(N^2),空间复杂度为O(1);
- 稳定性为稳定算法。
- 插入排序对小规模数据或基本有序数据比较高效,数据有序程度越高越高效。
4. 希尔排序
希尔排序也叫缩小增量排序,是插入排序的一种更高效的改进版本。插入排序对于大规模的乱序数组效率很慢,因为它每次只能将数据移动一位,希尔排序为了加快插入的速度,让数据移动的时候可以实现跳跃移动,节省了一部分的时间开支。
希尔排序在插入排序的基础上增加了一个增量的概念,即:插入排序只能与相邻的元素比较,而希尔排序是进行跳跃比较,增量就是步长。比如增量为3时,下标为0的元素与下标为3、6等依次加3的元素分为一组进行插入排序,下标为1的元素与下标为4、7…的元素分为一组进行插入排序。第一轮排序后,部分较大的数组往后靠,部分较小的数组往前靠。然后再减小增量,重复之前的步骤,直到增量为1,此时数组有序程度较高,运用插入排序效率也较高。
通过把数列进行分组,不停使用插入排序,直至从宏观上看起来有序,最后插入排序起来就容易了(无需多次移位或交换)。
C++实现:
void ShellSort(int *arr, int size)
{
int gap;
for(gap=size/2; gap>0; gap/=2) //gap为步长,每次减为原来的一半
{
//将数组分为gap组,每组进行直接插入排序
for(int i=0; i<gap; i++)
{
//内部是插入排序,只是将步长改为了gap
for(int j=i+gap; j<size; j+=gap)
{
int temp = arr[j];
int k;
for(k=j-gap; k>0; k-=gap)
{
if(temp < arr[k])
arr[k+gap]=arr[k];
else
break;
}
arr[k+gap]=temp;
}
}
}
}
- 希尔排序的复杂度和稳定性:
- 时间复杂度与增量的选取有关,增量为1是退化为直接排序时间复杂度为O(N^2),通过调整增量可使时间复杂度减到O(N^1.5);空间复杂度为O(1)。
- 希尔排序为不稳定排序。
5. 归并排序
归并算法的核心思想是分治法,将一个数组切成两半,递归切直到切成单个元素,然后重新组装合并,单个元素合并成两个元素,然后合并4个、8个…直到排序完成。
C++实现:
void mergeSort(int *arr, int *tempArr, int startIndex, int endIndex);
void merge(int *arr, int *tempArr, int startIndex, int middleIndex, int endIndex);
void mergeSort(int *arr,int size)
{
int *tempArr = new int[size];
mergeSort(arr, tempArr, 0, size-1);
}
void mergeSort(int *arr, int *tempArr, int startIndex, int endIndex)
{
if(endIndex <= startIndex)
return;
//中部下标
int middleIndex = startIndex + (endIndex-startIndex)/2;
//分解
mergeSort(arr,tempArr,startIndex,middleIndex);
mergeSort(arr,tempArr,middleIndex+1,endIndex);
//归并
merge(arr,tempArr,startIndex,middleIndex,endIndex);
}
void merge(int *arr,int *tempArr,int startIndex,int middleIndex,int endIndex)
{
//复制要合并的数据
for(int s=startIndex; s<=endIndex; s++)
tempArr[s] = arr[s];
int left = startIndex; //左边首位下标
int right = middleIndex+1; //右边首位下标
for(int k=startIndex; k<=endIndex; k++)
{
if(left > middleIndex)
//如果左边首位下标>中部下标,说明左边数据已经排完了
arr[k] = tempArr[right++];
else if(right > endIndex)
//如果右边首位下标>数组长度,说明右边数据已经排完了
arr[k] = tmepArr[left++];
else if(tempArr[right] < tempArr[left])
//将右边首位排入,然后右边下标指针+1
ar[k] = tempArr[right++];
else
//将左边首位排入,然后左边下标指针+1
arr[k] = tempArr[left++];
}
}
- 归并排序的复杂度和稳定性:
- 时间复杂度为O(N*log2N),空间复杂度为O(n);
- 归并排序为稳定排序。
6. 快速排序
快速排序的核心思想也是分治法。每次从序列中取一个基准值,其他数依次和基准值比较,大于基准值的放右边,小于基准值的放左边;然后再对左边和右边的两组数分别选出一个基准值,进行同样的比较移动;重复步骤,直到最后变成单个元素,整个数组就成了有序的序列。
- 单边扫描:
快速排序的关键之处在于切分,切分的同时要进行比较和移动,先介绍一种叫单边扫描的做法:
随意抽取一个数作为基准值,同时设定一个标记mark代表左边序列最右侧的下标位置(初始化为0),接下来遍历数组,如果元素大于基准值无需操作继续遍历,如果元素小于基准值,则把mark+1,再将mark所在位置的元素和遍历到的元素交换位置,mark这个位置存储的是比基准值小的数据,当遍历结束后,将基准值与mark所在元素交换位置即可。
C++实现:
void quickSort(int *arr, int startIndex, int endIndex);
int partition(int *arr, int startIndex, int endIndex);
void quickSort(int *arr, int size)
{
quickSort(arr, 0, size - 1);
}
void quickSort(int *arr, int startIndex, int endIndex)
{
if (endIndex <= startIndex)
return;
//切分
int pivotIndex = partition(arr, startIndex, endIndex);
quickSort(arr, startIndex, pivotIndex - 1);
quickSort(arr, pivotIndex + 1, endIndex);
}
int partition(int *arr, int startIndex, int endIndex)
{
int pivot = arr[startIndex];//取基准值
int mark = startIndex;//mark初始化为初始下标
for (int i = startIndex + 1; i <= endIndex; i++)
{
if (arr[i] < pivot)
{
//小于基准值则mark+1,并交换位置
mark++;
swap(arr[mark], arr[i]);
}
}
//基准值与mark对应元素交换位置
arr[startIndex] = arr[mark];
arr[mark] = pivot;
return mark;
}
- 双边扫描:
另外还有一种双边扫描的做法:随意抽取一个数作为基准值,然后从数组左右两边进行扫描,先从左往右找到一个大于基准值的元素将下标记录下来,然后转到从右往左扫描,找到一个小于基准值的元素,交换这两个元素的位置,重复步骤直到左右两个指针相遇,再将基准值与左侧最右边的元素交换。
C++实现:
void quickSort(int *arr, int startIndex, int endIndex);
int partition(int *arr, int startIndex, int endIndex);
void quickSort(int *arr, int size)
{
sort(arr, 0, size - 1);
}
void quickSort(int *arr, int startIndex, int endIndex)
{
if (endIndex <= startIndex)
return;
//切分:
int pivotIndex = partition(arr, startIndex, endIndex);
quickSort(arr, startIndex, pivotIndex - 1);
quickSort(arr, pivotIndex + 1, endIndex);
}
int partition(int *arr, int startIndex, int endIndex)
{
int left = startIndex;
int right = endIndex;
int pivot = arr[startIndex];//取第一个元素为基准值
while (true)
{
//从左往右扫描
while (arr[left] <= pivot)
{
left++;
if (left == right)
break;
}
//从右往左扫描
while (pivot < arr[right])
{
right--;
if (left == right)
break;
}
//左右指针相遇
if (left >= right)
break;
//交换左右数据
swap(arr[left], arr[right]);
//将基准值插入序列
swap(arr[startIndex], arr[right]);
return right;
}
}
- 快速排序复杂度和稳定性:
- 平均时间复杂度为O(Nlog2N),最坏时间复杂度为O(N^2);空间复杂度为O(Nlog2N);
- 快速排序为不稳定排序。
7. 堆排序
堆排序利用堆这种数据结构的特性进行排序,其中心思想是构造最大堆(or最小堆),即父节点总是大于其子节点,由于堆时刻要保持这种规则,所以一旦堆的数据发生变化,需要重新构建。
只需要每次从堆中取堆顶元素即为最大值,将剩下的数据重新构建后再取堆顶元素,以此类推直到将数据全部取完。
C++实现:
void buildHeap(int *arr, int size);
void sink(int *arr, int index; int size);
void heapSort(int *arr, int size)
{
buildHeap(arr,size);//构建堆
for(int i=size-1; i>0; i--)
{
//将堆顶元素与末位元素互换
swap(arr[0],arr[i]);
//数组长度-1隐藏堆尾元素
size--;
//将堆顶元素下沉,目的是将最大元素浮到堆顶来
sink(arr, 0, size);
}
}
void buildHeap(int *arr, int size)
{
for(int i=size/2; i>=0; i--)
sink(arr,i,size);
}
void sink(int *arr, int index, int size)
{
int leftChild = 2*index+1;//左子节点下标
int rightChild = 2*index+2;//右子节点下标
int present = index;//要调整的节点下标
//下沉左边
if(leftChild<size && arr[leftChild]>arr[present])
present = leftChild;
//下沉右边
if(rightChild<size && arr[rightChild]>arr[present])
present = rightChild;
//如果下标不相等,证明调换过了
if(present != index)
{
swap(arr[index],arr[present]);
sink(arr,present,size);//继续下沉
}
}
- 堆排序的复杂度和稳定性:
- 时间复杂度和快速排序一样为O(N^logN);堆排序是就地排序,空间复杂度为O(1)。
- 堆排序是不稳定排序。
8. 计数排序
基于比较的排序算法时间复杂度最好也只能降到O(N^logN),而计数排序是一种线性排序算法,不需要进行比较,时间复杂度为O(n+m)。(注意区别它和基数排序!)
计数排序的基本思想是排序之前先统计这组数中其他数小于这个数的个数,则可以确定这个数的位置。此算法需要辅助数组,是以空间换时间。
计数排序要求待排序的n个元素的大小在[0,k]之间,并且n与k在一个数量级上,此时可以吧时间复杂度降低到O(N)。
C++实现:
void countSort(int *arr, int size)
{
//找出数组中最大值
int max = arr[0];
for (int i = 0; i < size; i++)
if (arr[i] > max)
max = arr[i];
//初始化计数数组,将元素作为countArr的下标,统计每个元素出现次数
int *countArr = new int[max + 1] {0};
//计数
for (int i = 0; i < size; i++)
{
countArr[arr[i]]++;
arr[i] = 0;
}
//排序
int index = 0;
for (int i = 0; i < max+1; i++)
if (countArr[i] > 0)
arr[index++] = i;
}
- 计数排序复杂度和稳定性:
- 时间复杂度为O(N+M),M指数据量,计数排序算法的时间复杂度约等于O(N),快于任何比较型排序算法;空间复杂度为O(M).
- 计数排序是稳定排序算法。
- 计数排序局限性:计数排序只适用于正整数并且取值范围相差不大的数组排序使用,它的排序速度是非常可观的。
9. 桶排序
桶排序(或称为箱排序)可以看成是计数排序的升级版。将要排的数据分到多个桶里,每个桶里的数据再单独排序,再把每个桶的数据依次取出来,即可完成排序。
桶排序是用空间换时间,理论上来讲,桶的数量越多,时间复杂度就越低,当然空间复杂度越高。
C++实现:
#include<list>
#include<vector>
//在数据插入桶中的时候完成桶内排序:
void bucketInsert(list<int>& bucket, int val)
{
auto iter = bucket.begin();
while (iter != bucket.end() && val >= *iter)
iter++;
bucket.insert(iter, val); //insert会在iter之前插入数据,保证稳定排序
}
void bucketSort(int* arr, int size)
{
if (size <= 1)
return;
int min = arr[0], max = min;
for (int i = 1; i < size; ++i)
{
if (min > arr[i]) min = arr[i];
if (max < arr[i]) max = arr[i];
}
int bucketNum = (max - min) / size + 1; //向上取整,例如[0,9]有10个数,(9-0)/k+1=1
vector<list<int>> buckets(bucketNum);
for (int i = 0; i < size; i++)
{
int value = arr[i];
bucketInsert(buckets[(value - min) / size], value); //(value-min)/k就是在哪个桶里面
}
//写入原数组:
int index = 0;
for (int i = 0; i < bucketNum; ++i)
{
if (buckets[i].size())
for (auto& value : buckets[i])
arr[index++] = value;
}
}
- 桶排序的复杂度和稳定性:
- 平均时间复杂度为O(N+k),最佳时间复杂度为O(N+k),最差时间复杂度为O(N^2);空间复杂度为O(N*k)。
- 桶排序是稳定排序。(见代码中注释)
- 桶排序的思考及其应用:
- 在额外空间充足的情况下,尽量增大桶的数量,极限情况下每个桶只有一个数据时,完全避开了桶内排序的操作,桶排序的最好时间复杂度就能达到O(N)。比如高考总分750分,全国几百万人,我们只需要创建751个桶,循环一遍挨个扔进去,排序速度是毫秒级。
- 但是如果数据经过桶的划分之后,桶与桶的数据分布不均匀,有些数据非常多,有些数据非常少,比如[8,2,9,10,1,23,53,22,12,9000]这十个数据,我们分成十个桶,结果发现第一个桶装了9个数据,这是非常影响效率的情况,会使时间复杂度下降到O(N*logN),解决办法是我们每次桶内排序时判断一下数据量,如果桶里的数据量过大,那么应该在桶里面回调自身再进行一次桶排序。
10. 基数排序
基数排序是一种非比较整数型排序算法,其原理是将数据按位数切割成不同的数字,然后按每个位数分别比较。即:从个位开始,从低位到高位依次进行比较。基数排序可以看成是桶排序的扩展,也是用桶来辅助排序。
适用场景:假设要对100万个手机号进行排序,排的快的有归并、快排时间复杂度为O(N*logN),计数排序和桶排序虽然更快一些但占用内存大。这时候基数排序是最好的选择。
C++实现:
//求数据的最大位数,决定排序次数
int maxbit(int data[], int n)
{
int d = 1; //保存最大的位数
int p = 10;
for(int i = 0; i < n; ++i)
{
while(data[i] >= p)
{
p *= 10;
++d;
}
}
return d;
}
void radixsort(int data[], int n) //基数排序
{
int d = maxbit(data, n);
int tmp[n];
int count[10]; //计数器
int i, j, k;
int radix = 1;
for(i = 1; i <= d; i++) //进行d次排序
{
for(j = 0; j < 10; j++)
count[j] = 0; //每次分配前清空计数器
for(j = 0; j < n; j++)
{
k = (data[j] / radix) % 10; //统计每个桶中的记录数
count[k]++;
}
for(j = 1; j < 10; j++)
count[j] = count[j - 1] + count[j]; //将tmp中的位置依次分配给每个桶
for(j = n - 1; j >= 0; j--) //将所有桶中记录依次收集到tmp中
{
k = (data[j] / radix) % 10;
tmp[count[k] - 1] = data[j];
count[k]--;
}
for(j = 0; j < n; j++) //将临时数组的内容复制到data中
data[j] = tmp[j];
radix = radix * 10;
}
}
算法稳定性:假设在数列中存在a[i]=a[j],若在排序之前,a[i]在a[j]前面;并且排序之后,a[i]仍然在a[j]前面。则这个排序算法是稳定的。 ↩︎