title: 十大经典排序算法总结
date: 2018-12-16 15:54:20
updated: 2020-02-22 17:31:32
categories: 算法刷题
tags:
- 算法刷题
- 排序算法
此文档记录自己总结的十大经典排序算法,未完结更新中。
排序算法目录
编号 | 题目 |
---|---|
1 | 快速排序 |
2 | 冒泡排序 |
3 | 直接选择排序 |
4 | 堆排序 |
5 | 插入排序 |
6 | 希尔排序 |
7 | 归并排序 |
8 | 基数排序(todo) |
9 | 计数排序(todo) |
10 | 桶排序(todo) |
时间复杂度
1.快速排序
时间o(nlogn),空间o(logn)
partition思想代码:
//数组的快排。思路:partition思想
public static void quickSort(int[] num){
quickSortDetail(num, 0, num.length-1);
}
public static void quickSortDetail(int[] num, int left, int right){
if(left>=right){
return;
}
int index = partition(num, left, right);
quickSortDetail(num, left, index-1);
quickSortDetail(num, index+1, right);
}
public static int partition(int[] nums, int left, int right){
int pivot = nums[left];
swap(nums, left, right);
int store = left;
for(int i=left; i<right; ++i){// <right
if(nums[i]<=pivot){// <=pivot
swap(nums, i, store++);
}
}
swap(nums, right, store);
return store;
}
public static void swap(int[] nums, int i, int j){
int temp = nums[i];
nums[i] = nums[j];
nums[j] = temp;
}
模板型代码:
void quickSort(int[] num, int left, int right){
if(left>right){
return;
}
int i = left;
int j = right;
int x = num[left];
while(i<j){
while(i<j && num[j]>=x){
j--;
}
if(i<j){
num[i++] = num[j];
}
while(i<j && num[i]<=x){
i++;
}
if(i<j){
num[j--] = num[i];
}
}
num[i] = x;
quickSort(num, left, i-1);
quickSort(num, i+1, right);
}
2.冒泡排序
冒泡排序思想
交换排序:两两比较待排序的关键字,并交换不满足次序要求的那对数,直到整个表都满足次序要求为止。
冒泡排序是一种交换排序。它重复地走访过要排序的数列,一次比较两个元素,如果他们的顺序错误就把他们交换过来。走访数列的工作是重复地进行直到没有再需要交换,也就是说该数列已经排序完成。
这个算法的名字由来是因为越小的元素会经由交换慢慢“浮”到数列的顶端,故名。
假设有一个大小为 N 的无序序列。冒泡排序就是要每趟排序过程中通过两两比较,找到第 i 个小(大)的元素,将其往上排。
以上图为例,演示一下冒泡排序的实际流程:
假设有一个无序序列 { 4. 3. 1. 2, 5 }
第一趟排序:通过两两比较,找到第一小的数值 1 ,将其放在序列的第一位。
第二趟排序:通过两两比较,找到第二小的数值 2 ,将其放在序列的第二位。
第三趟排序:通过两两比较,找到第三小的数值 3 ,将其放在序列的第三位。
至此,所有元素已经有序,排序结束。
要将以上流程转化为代码,我们需要像机器一样去思考,不然编译器可看不懂。
假设要对一个大小为 N 的无序序列进行升序排序(即从小到大)。
(1) 每趟排序过程中需要通过比较找到第 i 个小的元素。
所以,我们需要一个外部循环,从数组首端(下标 0) 开始,一直扫描到倒数第二个元素(即下标 N - 2) ,剩下最后一个元素,必然为最大。
(2) 假设是第 i 趟排序,可知,前 i-1 个元素已经有序。现在要找第 i 个元素,只需从数组末端开始,扫描到第 i 个元素,将它们两两比较即可。
所以,需要一个内部循环,从数组末端开始(下标 N - 1),扫描到 (下标 i + 1)。
原始版本代码
public static void bubbleSort(int[] arr){
for(int i=0; i<arr.length-1; ++i){//循环n-1趟,每趟最大的沉到最后
boolean swapCnt = false;
for(int j=0; j<arr.length-i-1; ++j){//每一趟的相邻元素比较交换
if(arr[j]>arr[j+1]){
swap(arr, j, j+1);
swapCnt = true;
}
}
if(swapCnt==false){//如果没发生交换,则说明已经有序,直接退出,这样有可能达到o(n)
return;
}
}
}
同样的版本另外一种写法:
public void bubbleSort(int[] list) {
int temp = 0; // 用来交换的临时数
// 要遍历的次数
for (int i = 0; i < list.length - 1; i++) {
// 从后向前依次的比较相邻两个数的大小,遍历一次后,把数组中第i小的数放在第i个位置上
for (int j = list.length - 1; j > i; j--) {
// 比较相邻的元素,如果前面的数大于后面的数,则交换
if (list[j - 1] > list[j]) {
temp = list[j - 1];
list[j - 1] = list[j];
list[j] = temp;
}
}
System.out.format("第 %d 趟: ", i);
printAll(list);
}
}
算法性能分析
冒泡排序算法的性能如下图
时间复杂度
若文件的初始状态是正序的,一趟扫描即可完成排序。所需的关键字比较次数C和记录移动次数M均达到最小值:Cmin = N - 1, Mmin = 0。所以,冒泡排序最好时间复杂度为O(N)。
若初始文件是反序的,需要进行 N -1 趟排序。每趟排序要进行 N - i 次关键字的比较(1 ≤ i ≤ N - 1),且每次比较都必须移动记录三次来达到交换记录位置。在这种情况下,比较和移动次数均达到最大值:
Cmax = N(N-1)/2 = O(N2)
Mmax = 3N(N-1)/2 = O(N2)
冒泡排序的最坏时间复杂度为O(N2)。
因此,冒泡排序的平均时间复杂度为O(N2)。
总结起来,其实就是一句话:当数据越接近正序时,冒泡排序性能越好。
算法稳定性
冒泡排序就是把小的元素往前调或者把大的元素往后调。比较是相邻的两个元素比较,交换也发生在这两个元素之间。
所以相同元素的前后顺序并没有改变,所以冒泡排序是一种稳定排序算法。
优化版本代码
对冒泡排序常见的改进方法是加入标志性变量exchange,用于标志某一趟排序过程中是否有数据交换。
如果进行某一趟排序时并没有进行数据交换,则说明所有数据已经有序,可立即结束排序,避免不必要的比较过程。
/ 对 bubbleSort 的优化算法
public void bubbleSort_2(int[] list) {
int temp = 0; // 用来交换的临时数
boolean bChange = false; // 交换标志
// 要遍历的次数
for (int i = 0; i < list.length - 1; i++) {
bChange = false;
// 从后向前依次的比较相邻两个数的大小,遍历一次后,把数组中第i小的数放在第i个位置上
for (int j = list.length - 1; j > i; j--) {
// 比较相邻的元素,如果前面的数大于后面的数,则交换
if (list[j - 1] > list[j]) {
temp = list[j - 1];
list[j - 1] = list[j];
list[j] = temp;
bChange = true;
}
}
// 如果标志为false,说明本轮遍历没有交换,已经是有序数列,可以结束排序
if (false == bChange)
break;
System.out.format("第 %d 趟: ", i);
printAll(list);
}
}
3.直接选择排序
时间o(n^2),空间o(1)
首先在未排序序列中找到最小元素,存放到排序序列的起始位置,然后,再从剩余未排序元素中继续寻找最小元素,然后放到已排序序列的末尾,以此类推,直到所有元素均排序完毕。
//1.选择排序
public static void pickSort(int[] arr){
for(int i=0; i<arr.length-1; ++i){
int minIndex = i;
for(int j=i+1; j<arr.length; ++j){
if(arr[j] < arr[minIndex]){
minIndex = j;
}
}
swap(arr, i, minIndex);
}
}
4.堆排序
来源: 堆排序就这么简单
堆排序是利用堆这种数据结构而设计的一种排序算法,堆排序是一种选择排序,它的最坏,最好,平均时间复杂度均为O(nlogn),它也是不稳定排序。空间o(1)
堆排序的基本思路:
a.将无需序列构建成一个堆,根据需求选择大顶堆(升序)或小顶堆(降序);
b.将堆顶元素与末尾元素交换,将最大元素"沉"到数组末端;
c.重新调整剩下元素结构,使其满足堆定义,然后继续交换堆顶元素与当前末尾元素,反复执行调整+交换步骤,直到整个序列有序。
堆是具有以下性质的完全二叉树:每个结点的值都大于或等于其左右孩子结点的值,称为大顶堆;或者每个结点的值都小于或等于其左右孩子结点的值,称为小顶堆
对于完全二叉树对应的数组来说,堆的定义就是:
大顶堆:arr[i] >= arr[2i+1] && arr[i] >= arr[2i+2]
小顶堆:arr[i] <= arr[2i+1] && arr[i] <= arr[2i+2]
//堆排序。思路:基于完全二叉树性质进行堆排序,时间o(nlogn),空间o(1)
public static void heapSort(int[] nums){
if(nums==null || nums.length<2){
return;
}
//1.先建好大根堆:从第一个非叶子结点从下至上,从右至左调整结构
for(int i=nums.length/2-1; i>=0; --i){
adjustHeap(nums, i, nums.length);
}
//2.堆排序。每次将堆顶与末尾元素交换并调整堆
for(int i=nums.length-1; i>0; --i){
swap(nums, 0, i);//将堆顶元素与末尾元素进行交换
adjustHeap(nums, 0, i);//重新对堆进行调整,注意这里第三个参数不是length而是i,因为i之后的已经有序
}
}
//调整堆(仅是调整i节点的过程,建立在大顶堆已构建的基础上)
public static void adjustHeap(int[] nums, int i, int length){
int temp = nums[i];
for(int k=2*i+1; k<length; k=2*i+1){
if(k+1<length && nums[k+1]>nums[k]){//找左右子节点中最大的
k++;
}
if(nums[k]>temp){//子节点比父节点大,需要交换,这里直接赋值,最后再赋值一次即可,减少交换次数
nums[i] = nums[k];
i = k;
}
else{//如果顺序正常,则break
break;
}
}
nums[i] = temp;
}
public static void swap(int[] nums, int i, int j){
int temp = nums[i];
nums[i] = nums[j];
nums[j] = temp;
}
public static void main(String[] args){
int[] arr = new int[]{6,4,3,1,11,54,2};
System.out.println(Arrays.toString(arr));
heapSort(arr);
System.out.println(Arrays.toString(arr));
//out:
//[6, 4, 3, 1, 11, 54, 2]
//[1, 2, 3, 4, 6, 11, 54]
}
5.插入排序
时间o(n^2),空间o(1)
通过构建有序序列(从第一个元素开始,该元素可以认为已经被排序),对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。
//3.插入排序
public static void insertSort(int[] arr){
if(arr==null || arr.length<2){
return;
}
for(int i=1; i<arr.length; ++i){
int temp = arr[i];
int j = i;
while(j-1>=0 && arr[j-1]>temp){//如果当前元素大于temp,该元素后移
arr[j] = arr[j-1];
j--;
}
arr[j] = temp; //找到temp应该在的位置
}
}
6.希尔排序
时间o(n^(1.3—2)),空间o(1)
希尔排序也是一种插入排序,它是简单插入排序经过改进之后的一个更高效的版本,也称为缩小增量排序,该算法是冲破O(n2)的第一批算法之一
简单插入排序很循规蹈矩,不管数组分布是怎么样的,依然一步一步的对元素进行比较,移动,插入,
比如[5,4,3,2,1,0]这种倒序序列,数组末端的0要回到首位置很是费劲,比较和移动元素均需n-1次。
而希尔排序在数组中采用跳跃式分组的策略,通过某个增量将数组元素划分为若干组,然后分组进行插入排序,
随后逐步缩小增量,继续按组进行插入排序操作,直至增量为1。
希尔排序通过这种策略使得整个数组在初始阶段达到从宏观上看基本有序,小的基本在前,大的基本在后。
然后缩小增量,到增量为1时,其实多数情况下只需微调即可,不会涉及过多的数据移动。
常用增量:选择增量gap=length/2,缩小增量继续以gap = gap/2的方式,最后增量为1
//4.希尔排序(缩小增量排序)--两种希尔排序
//希尔排序:针对有序序列在插入时采用交换法
public static void shellSort(int []arr){
//增量gap,并逐步缩小增量
for(int gap=arr.length/2;gap>0;gap/=2){
//从第gap个元素,逐个对其所在组进行直接插入排序操作
for(int i=gap;i<arr.length;i++){
int j = i;
while(j-gap>=0 && arr[j]<arr[j-gap]){
//插入排序采用交换法
swap(arr,j,j-gap);
j-=gap;
}
}
}
}
//希尔排序:针对有序序列在插入时采用移动法。
public static void shellSort2(int []arr){
//增量gap,并逐步缩小增量
for(int gap=arr.length/2;gap>0;gap/=2){
//从第gap个元素,逐个对其所在组进行直接插入排序操作
for(int i=gap;i<arr.length;i++){
int j = i;
int temp = arr[j];
if(arr[j]<arr[j-gap]){
while(j-gap>=0 && temp<arr[j-gap]){
//移动法
arr[j] = arr[j-gap];
j-=gap;
}
arr[j] = temp;
}
}
}
}
7.归并排序
时间o(nlogn),空间o(n),稳定
public class Solution {
//归并排序
public static void mergeSort(int[] arr){
int[] temp = new int[arr.length]; //在排序前,先建好一个长度等于原数组长度的临时数组,避免递归中频繁开辟空间
sort(arr, 0, arr.length-1, temp);
}
private static void sort(int[] arr, int left, int right, int[] temp){
if(left<right){
int mid = (left+right)/2;
sort(arr, left, mid, temp); //左边归并排序,使得左子序列有序
sort(arr, mid+1, right, temp);//右边归并排序,使得右子序列有序
merge(arr, left, mid, right, temp);//将两个有序子数组合并操作
}
}
private static void merge(int[] arr, int left, int mid, int right, int[] temp){
int i = left; //左序列指针
int j = mid+1; //右序列指针
int t = 0; //临时数组指针
while(i<=mid && j<=right){
if(arr[i]<=arr[j]){
temp[t++] = arr[i++];
}
else{
temp[t++] = arr[j++];
}
}
while(i<=mid){//将左边剩余元素填充进temp中
temp[t++] = arr[i++];
}
while(j<=right){//将右边剩余元素填充进temp中
temp[t++] = arr[j++];
}
t = 0;
//将temp中的元素全部拷贝到原数组中
while(left <= right){
arr[left++] = temp[t++];
}
}
public static void show(int[] arr){
for(int i=0; i<arr.length; ++i){
System.out.print(arr[i] + " ");
}
System.out.println();
}
public static void main(String[] args){
int[] arr = {9,8,7,6,5,4,3,2,1,123,23};
show(arr);//out:9 8 7 6 5 4 3 2 1 123 23
mergeSort(arr);
show(arr); //out:1 2 3 4 5 6 7 8 9 23 123
}
}
另一个版本:
import java.util.Arrays;
public class Solution {
//归并排序。思路:分而治之,先分后治,辅助数组temp[]
public static void mergeSort(int[] nums){
if(nums==null || nums.length<2){
return;
}
int[] temp = new int[nums.length]; //在排序前,先建好一个长度等于原数组长度的临时数组,避免递归中频繁开辟空间
mergeSortDetail(nums, 0, nums.length-1, temp);
}
//分
private static void mergeSortDetail(int[] nums, int left, int right, int[] temp){
if(left<right){//一个数字不用merge,只有两个以上才merge
int mid = (left+right)/2;
mergeSortDetail(nums, left, mid, temp);//左边归并排序,使得左子序列有序
mergeSortDetail(nums, mid+1, right, temp);//右边归并排序,使得右子序列有序
merge(nums, left, mid, right, temp);//将两个有序子数组合并操作
}
}
//治
private static void merge(int[] nums, int left, int mid, int right, int[] temp){
int tempIndex = 0;//temp数组的当前位置
int i = left; //左序列的索引
int j = mid+1; //右序列的索引
while(i<=mid && j<=right){//将左、右序列的元素有序的放进temp数组中
temp[tempIndex++] = nums[i]<=nums[j] ? nums[i++] : nums[j++];
}
while(i<=mid){//将左边剩余元素填充进temp中
temp[tempIndex++] = nums[i++];
}
while(j<=right){//将右边剩余元素填充进temp中
temp[tempIndex++] = nums[j++];
}
//将temp中的元素全部拷贝到nums原数组中
tempIndex = 0;
while(left<=right){
nums[left++] = temp[tempIndex++];
}
}
public static void main(String []args){
int[] nums = {9,8,7,6,5,4,3,2,1};
mergeSort(nums);
System.out.println(Arrays.toString(nums));
}
}