十大排序算法总结
文章目录
名词解释
- 稳定 :如果a原本在b前面,而a=b,排序之后a仍然在b的前面;
- 不稳定 :如果a原本在b的前面,而a=b,排序之后a可能会出现在b的后面;
- 内排序 :所有排序操作都在内存中完成;
- 外排序 :由于数据太大,因此把数据放在磁盘中,而排序通过磁盘和内存的数据传输才能进行;
- 图片名词解释:
- n: 数据规模
- k: “桶”的个数
- In-place: 占用常数内存,不占用额外内存
- Out-place: 占用额外内存
1、冒泡排序
1.1 算法步骤
- 步骤1: 比较相邻的元素。如果第一个比第二个大,就交换它们两个;
- 步骤2: 对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对,这样在最后的元素应该会是最大的数;
- 步骤3: 针对所有的元素重复以上的步骤,除了最后一个;
- 步骤4: 重复步骤1~3,直到排序完成。
1.2 动图演示
1.3 代码实现
#include <bits/stdc++.h>
using namespace std;
int main(){
int arr[10] = {2,8,4,9,0,12,16,1,7,6};
// 第一层循环是代表要循环比较的趟数
for (int i = 0; i < 10-1; ++i) {
// 每一层循环要比较的次数
for (int j = 0; j < 10-1-i; ++j) {
// 大的数据往上浮动
if (arr[j+1]<arr[j])
swap(arr[j+1],arr[j]);
}
}
for (int k = 0; k < 10; ++k) {
cout<<arr[k]<<" ";
}
return 0;
}
1.4 时间复杂度
- 最佳情况:T(n) = O(n)
- 最差情况:T(n) = O(n2)
- 平均情况:T(n) = O(n2)
2、选择排序
2.1 算法步骤
- 首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置。
- 再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。
- 重复第二步,直到所有元素均排序完毕
2.2 动图演示
2.3 代码实现
#include <bits/stdc++.h>
using namespace std;
int main(){
int arr[10] = {2,8,4,9,0,12,16,1,7,6};
// 第一层循环是代表要循环比较的趟数
for (int i = 0; i < 10-1; ++i) {
// 记录最小的下标
int index = i;
for (int j = i+1; j < 10; ++j) {
// 更新最小的下标
if (arr[index]>arr[j])
index=j;
}
swap(arr[i],arr[index]);
}
for (int k = 0; k < 10; ++k) {
cout<<arr[k]<<" ";
}
return 0;
}
2.4 时间复杂度
- 最佳情况:T(n) = O(n2)
- 最差情况:T(n) = O(n2)
- 平均情况:T(n) = O(n2)
3、插入排序
3.1 算法步骤
- 将第一待排序序列第一个元素看做一个有序序列,把第二个元素到最后一个元素当成是未排序序列。
- 从头到尾依次扫描未排序序列,将扫描到的每个元素插入有序序列的适当位置。(如果待插入的元素与有序序列中的某个元素相等,则将待插入元素插入到相等元素的后面。)
3.2 动图演示
3.3 代码实现
#include <bits/stdc++.h>
using namespace std;
int main(){
int arr[15] = {2,8,4,9,0,12,16,1,7,6,6,8,45,-7,-3};
int len = 15;
// 将1~len的数组看做是待排序的数组序列
for (int i = 1; i < len; ++i) {
int j=i-1;
// 将i插入到合适的位置
while (j>=0&&arr[j+1]<arr[j]){
swap(arr[j+1],arr[j]);
j--;
}
}
for (int k = 0; k < len; ++k) {
cout<<arr[k]<<" ";
}
return 0;
}
3.4 时间复杂度
- 最佳情况:T(n) = O(n)
- 最坏情况:T(n) = O(n2)
- 平均情况:T(n) = O(n2)
4、希尔排序
4.1 算法步骤
- 选择一个增量序列 t1,t2,……,tk,其中 ti > tj, tk = 1;
- 按增量序列个数 k,对序列进行 k 趟排序;
- 每趟排序,根据对应的增量 ti,将待排序列分割成若干长度为 m 的子序列,分别对各子表进行直接插入排序。仅增量因子为 1 时,整个序列作为一个表来处理,表长度即为整个序列的长度。
4.2 动图演示
4.3 代码演示
#include <bits/stdc++.h>
using namespace std;
int main(){
int arr[15] = {2,8,4,9,0,12,16,1,7,6,6,8,45,-7,-3};
int len = 15;
int gap = len/2;
// 初始增量gap=len/2,按增量序列个数k,对序列进行 k 趟排序;
while (gap>0){
// 对每一个序列进行插入排序
for (int i = gap; i < len; ++i) {
int j = i-gap;
while (j>=0&&arr[j]>arr[j+gap]){
swap(arr[j],arr[j+gap]);
j-=gap;
}
}
gap/=2;
}
for (int k = 0; k < len; ++k) {
cout<<arr[k]<<" ";
}
return 0;
}
4.4 时间复杂度
- 最佳情况:T(n) = O(nlog2 n)
- 最坏情况:T(n) = O(nlog2 n)
- 平均情况:T(n) =O(nlog2n)
5、归并排序
5.1 算法步骤
- 申请空间,使其大小为两个已经排序序列之和,该空间用来存放合并后的序列;
- 设定两个指针,最初位置分别为两个已经排序序列的起始位置;
- 比较两个指针所指向的元素,选择相对小的元素放入到合并空间,并移动指针到下一位置;
- 重复步骤 3 直到某一指针达到序列尾;
- 将另一序列剩下的所有元素直接复制到合并序列尾。
5.2 动图演示
5.3 代码实现
#include <bits/stdc++.h>
using namespace std;
int tmp[15];
int arr[15]={2,8,4,9,0,12,16,1,7,6,6,8,45,-7,-3};
void merge_sort(int a[], int l, int r)
{
if (l >= r)
return;
int mid = (l + r) / 2;
merge_sort(a, l, mid);
merge_sort(a, mid + 1, r);
int k = 0, i = l,j = mid + 1;
while (i <= mid && j <= r)
{
if (a[i] < a[j])
tmp[k++] = a[i++];
else
tmp[k++] = a[j++];
}
while (i <= mid)
tmp[k++] = a[i++];
while (j <= r)
tmp[k++] = a[j++];
for (i = l, j = 0; i <= r; ++i, ++j)
a[i] = tmp[j];
}
int main(){
int len = 15;
merge_sort(arr,0,len-1);
for (int k = 0; k < len; ++k) {
cout<<arr[k]<<" ";
}
return 0;
}
5.4 时间复杂度
- 最佳情况:T(n) = O(n)
- 最差情况:T(n) = O(nlogn)
- 平均情况:T(n) = O(nlogn)
6、快速排序
6.1 算法步骤
- 从数列中挑出一个元素,称为 “基准”(pivot);
- 重新排序数列,所有元素比基准值小的摆放在基准前面,所有元素比基准值大的摆在基准的后面(相同的数可以到任一边)。在这个分区退出之后,该基准就处于数列的中间位置。这个称为分区(partition)操作;
- 递归地(recursive)把小于基准值元素的子数列和大于基准值元素的子数列排序;
6.2 动图演示
6.3 代码演示
#include <bits/stdc++.h>
using namespace std;
int arr[15]={2,8,4,9,0,12,16,1,7,6,6,8,45,-7,-3};
void quick_sort(int a[], int l, int r)
{
if (l >= r)
return;
int flag = a[(l+r)/2], i = l - 1, j = r + 1;
while (i < j)
{
do
i++;
while (a[i] < flag);
do
j--;
while (a[j] > flag);
if (i < j)
swap(a[i], a[j]);
else
break;
}
quick_sort(a, l, j);
quick_sort(a, j + 1, r);
}
int main(){
int len = 15;
quick_sort(arr,0,len-1);
for (int k = 0; k < len; ++k) {
cout<<arr[k]<<" ";
}
return 0;
}
6.4 时间复杂度
- 最佳情况:T(n) = O(nlogn)
- 最差情况:T(n) = O(n2)
- 平均情况:T(n) = O(nlogn)
7、堆排序
7.1 算法步骤
- 创建一个堆 H[0……n-1];
- 把堆首(最大值)和堆尾互换;
- 把堆的尺寸缩小 1,并调用 shift_down(0),目的是把新的数组顶端数据调整到相应位置;
- 重复步骤 2,直到堆的尺寸为 1。
7.2 动图演示
7.3 代码实现
#include <bits/stdc++.h>
using namespace std;
int a[15] = {2, 8, 4, 9, 0, 12, 16, 1, 7, 6, 6, 8, 45, -7, -3};
int alen = 15;
// 构造大根堆,通过新插入的数上升
void heapInsert(int arr[], int len) {
for (int i = 0; i < len; ++i) {
// 当前插入的索引
int currentIndex = i;
// 父结点
int fatherIndex = (currentIndex - 1) / 2;
// 如果当前插入的值大于其父亲结点的值,则交换值,并且将索引指向父节点
// 然后继续和上面的父节点值比较,直到不大于父亲结点,则退出循环
while (arr[currentIndex] > arr[fatherIndex]) {
//交换当前结点与父结点的值
swap(arr[fatherIndex], arr[currentIndex]);
//将当前索引指向父索引
currentIndex = fatherIndex;
//将当前索引指向父索引
fatherIndex = (currentIndex - 1) / 2;
}
}
}
//将剩余的数构造成大根堆(通过顶端的数下降)
void heapify(int arr[], int index, int size) {
int left = 2 * index + 1;
int right = 2 * index + 2;
while (left < size) {
//判断孩子中较大的值的索引(要确保右孩子在size范围之内)
int largestIndex = arr[left] < arr[right] && right < size ? right : left;
//判断孩子中较大的值的索引(要确保右孩子在size范围之内)
if (arr[index] > arr[largestIndex]) {
largestIndex = index;
}
//如果父结点索引是最大值的索引,那已经是大根堆了,则退出循环
if (index == largestIndex)
break;
//父结点不是最大值,与孩子中较大的值交换
swap(arr[largestIndex], arr[index]);
//父结点不是最大值,与孩子中较大的值交换
index = largestIndex;
//重新计算交换之后的孩子的索引
left = 2 * index + 1;
right = 2 * index + 2;
}
}
// 堆排序
void heapSort(int arr[], int len) {
//构造大根堆
heapInsert(arr, len);
while (len > 1) {
//构造大根堆
swap(arr[0], arr[len - 1]);
len--;
//构造大根堆
heapify(arr, 0, len);
}
}
int main() {
heapSort(a, alen);
for (int i = 0; i < alen; ++i) {
cout << a[i] << " ";
}
return 0;
}
7.4 时间复杂度
- 最佳情况:T(n) = O(nlogn)
- 最差情况:T(n) = O(nlogn)
- 平均情况:T(n) = O(nlogn)
8、计数排序
8.1 算法步骤
- (1)找出待排序的数组中最大和最小的元素
- (2)统计数组中每个值为i的元素出现的次数,存入数组C的第i项
- (3)对所有的计数累加(从C中的第一个元素开始,每一项和前一项相加)
- (4)反向填充目标数组:将每个元素i放在新数组的第C(i)项,每放一个元素就将C(i)减去
8.2 动图演示
8.3 代码实现
#include <bits/stdc++.h>
using namespace std;
int a[15] = {2, 8, 4, 9, 0, 12, 16, 1, 7, 6, 6, 8, 45, -7, -3};
int alen = 15;
void CountingSort(int arr[],int len){
int bias,cmin=arr[0],cmax=arr[0];
// 找出整个数组里面最大值和最小值
for (int i = 0; i < len; ++i) {
if(arr[i]>cmax)
cmax=arr[i];
if(arr[i]<cmin)
cmin=arr[i];
}
// 记录偏移量
bias = -cmin;
// 创建bucket数组,记录每个arr位置元素出现的次数
int bucket[cmax-cmin+1];
memset(bucket,0,sizeof(bucket));
for (int j = 0; j < len; ++j) {
bucket[arr[j]+bias]++;
}
int index=0,i=0;
// 从bucket里面还原出arr内容
while (index<len){
if (bucket[i]!=0){
arr[index]=i-bias;
bucket[i]--;
index++;
} else
i++;
}
}
int main() {
CountingSort(a,alen);
for (int i = 0; i < alen; ++i) {
cout << a[i] << " ";
}
return 0;
}
8.4 时间复杂度
- 最佳情况:T(n) = O(n+k)
- 最差情况:T(n) = O(n+k)
- 平均情况:T(n) = O(n+k)
9、桶排序
1. 什么时候最快
当输入的数据可以均匀的分配到每一个桶中。
2. 什么时候最慢
当输入的数据被分配到了同一个桶中。
3. 示意图
元素分布在桶中:
然后,元素在每个桶中排序:
9.3 代码实现
import java.util.ArrayList;
public class BucketSort {
//在链表中添加元素的同时需要进行元素的排序
public static void sort(ArrayList<Integer> list, int i) {
if(list==null)
list.add(i);
//这里采用的排序方式为插入排序
else {
int flag=list.size()-1;
while(flag>=0&&list.get(flag)>i) {
if(flag+1>=list.size())
list.add(list.get(flag));
else
list.set(flag+1, list.get(flag));
flag--;
}
if(flag != (list.size()-1))
//注意这里是flag+1,自己可以尝试将这里换成flag看看,会出现数组越界的情况
list.set(flag+1, i);
else
list.add(i);
}
}
public static void Bucketsort(int []num,int sum) {
//遍历得到数组中的最大值与最小值
int min=Integer.MAX_VALUE;
int max=Integer.MIN_VALUE;
for(int i=0;i<num.length;i++) {
min = min <= num[i] ? min: num[i];
max = max >= num[i] ? max: num[i];
}
//求出每个桶的长度,这里必须使用Double
double size=(double)(max-min+1)/sum;
ArrayList<Integer>list[]=new ArrayList[sum];
for(int i=0;i<sum;i++) {
list[i]=new ArrayList<Integer>();
}
//将每个元素放入对应的桶之中同时进行桶内元素的排序
for(int i=0;i<num.length;i++) {
System.out.println("元素:"+String.format("%-2s", num[i])+", 被分配到"+(int)Math.floor((num[i]-min)/size)+"号桶");
sort(list[(int)Math.floor((num[i]-min)/size)], num[i]);
}
System.out.println();
for(int i=0;i<sum;i++) {
System.out.println(String.format("%-1s", i)+"号桶内排序:"+list[i]);
}
System.out.println();
//顺序遍历各个桶,得出我们 已经排序号的序列
for(int i=0;i<list.length;i++) {
if(list[i]!=null){
for(int j=0;j<list[i].size();j++) {
System.out.print(list[i].get(j)+" ");
}
}
}
System.out.println();
}
public static void main(String[] args) {
int []num ={7,4,3,2,6,5,-2,-1,4,5,6,1,7,3};
//这里桶的数量可以你自己定义,这里我就定义成了3
Bucketsort(num, 5);
}
}
9.4 时间复杂度
- 最佳情况:T(n) = O(n+k)
- 最差情况:T(n) = O(n+k)
- 平均情况:T(n) = O(n2)
10、基数排序
10.1 算法描述
- 步骤1:取得数组中的最大数,并取得位数;
- 步骤2:arr为原始数组,从最低位开始取每个位组成radix数组;
- 步骤3:对radix进行计数排序(利用计数排序适用于小范围数的特点);
10.2 动图演示
10.3 代码实现
/**
* 基数排序
* @param array
* @return
*/
public static int[] RadixSort(int[] array) {
if (array == null || array.length < 2)
return array;
// 1.先算出最大数的位数;
int max = array[0];
for (int i = 1; i < array.length; i++) {
max = Math.max(max, array[i]);
}
int maxDigit = 0;
while (max != 0) {
max /= 10;
maxDigit++;
}
int mod = 10, div = 1;
ArrayList<ArrayList<Integer>> bucketList = new ArrayList<ArrayList<Integer>>();
for (int i = 0; i < 10; i++)
bucketList.add(new ArrayList<Integer>());
for (int i = 0; i < maxDigit; i++, mod *= 10, div *= 10) {
for (int j = 0; j < array.length; j++) {
int num = (array[j] % mod) / div;
bucketList.get(num).add(array[j]);
}
int index = 0;
for (int j = 0; j < bucketList.size(); j++) {
for (int k = 0; k < bucketList.get(j).size(); k++)
array[index++] = bucketList.get(j).get(k);
bucketList.get(j).clear();
}
}
return array;
}
10.4 时间复杂度
-
最佳情况:T(n) = O(n * k
-
最差情况:T(n) = O(n * k)
-
平均情况:T(n) = O(n * k)
10.5 基数排序有两种方法:
-
MSD 从高位开始进行排序
-
LSD 从低位开始进行排序
-
基数排序 vs 计数排序 vs 桶排序
这三种排序算法都利用了桶的概念,但对桶的使用方法上有明显差异:
- 基数排序: 根据键值的每位数字来分配桶
- 计数排序: 每个桶只存储单一键值
- 桶排序: 每个桶存储一定范围的数值