常用排序算法


排序算法大体可分为两种:
一种是比较排序,时间复杂度O(nlogn) ~ O(n^2),主要有: 冒泡排序,选择排序,插入排序,归并排序,堆排序,快速排序等。
另一种是非比较排序,时间复杂度可以达到O(n),主要有: 计数排序,基数排序,桶排序等。

下面介绍常用的排序算法(升序排序)。
在这里插入图片描述

1. 冒泡排序(Bubble Sort)

初版冒泡排序

思路:对列表多次重复遍历,每相邻的两个数进行比较,大的往后移,就像冒泡一样,大的慢慢浮到后面去。

如下图:
冒泡排序

所有代码使用到的swap函数:

/**
 * 交换 i j 两个下标的值
 * @param arr int[]
 * @param i 下标
 * @param j 下标
 */
public static void swap(int[] arr, int i, int j) {
    int t = arr[i];
    arr[i] = arr[j];
    arr[j] = t;
}

冒泡排序实现代码:

/**
 * 最差时间复杂度 ---- O(n^2)
 * 最优时间复杂度 ---- O(n) - 如果能在内部循环第一次运行时,使用一个旗标来表示有无需要交换的可能,可以把最优时间复杂度降低到O(n)
 * 平均时间复杂度 ---- O(n^2)
 * 所需辅助空间 ------ O(1)
 * 稳定性 ----------- 稳定
 */
public static void bubbleSort(int[] arr){
	int len = arr.length;
	for (int i = 0; i < len; i++) {
	    for (int j = 0; j < len - 1 - i; j++) {
	        if (arr[j] > arr[j+1]) {
                swap(arr, j, j+1);
	        }
	    }
	}
}

升级版:鸡尾酒冒泡排序

思路:遍历方式从低到高然后从高到低,从前到后再从后到前,先找最大的再找最小的。

在这里插入图片描述

/**
 * 最差时间复杂度 ---- O(n^2)
 * 最优时间复杂度 ---- O(n),如果序列在一开始已经大部分排序过的话,会接近O(n)
 * 平均时间复杂度 ---- O(n^2)
 * 所需辅助空间 ------ O(1)
 * 稳定性 ------------ 稳定
 */
public static void CocktailSort(int[] arr) {
    int left = 0, len = arr.length;        // 初始化边界
    int right = len - 1;
    while (left < right) {
        // 前半轮,将最大元素放到后面
        for (int i = left; i < right; i++) {
            if (arr[i] > arr[i + 1]) {
                swap(arr, i, i+1);//交换两个位置的数值
            }
        }
        right--;
        // 后半轮,将最小元素放到前面
        for (int i = right; i > left; i--) {
            if (arr[i - 1] > arr[i]) {
                swap(arr, i, i-1);//交换两个位置的数值
            }
        }
        left++;
    }
}

2. 选择排序(Selection Sort)

思路:第一趟遍历完记录最小的数,放置到第一位(交换,当已在第一位时不交换);继续遍历并记录剩余列表中的最小数,放置到第二位(交换);以此类推。

使用下标记录当前最小值即可。

在这里插入图片描述

/**
 * 最差时间复杂度 ----- O(n^2)
 * 最优时间复杂度 ----- O(n^2)
 * 平均时间复杂度 ----- O(n^2)
 * 所需辅助空间 ------- O(1)
 * 稳定性 ------------ 不稳定
 */
public static void selectionSort(int[] arr) {
    int len = arr.length;
    for (int i = 0; i < len - 1; i++) {
        int idx = i; //记录最小值的下标
        // 从 下标为i+1 往后寻找当前最小值的下标
        for (int j = i + 1; j < len; j++) {
            if (arr[idx] > arr[j]){
                idx = j;
            }
        }
        //找到了比arr[i]小的则与arr[i]交换位置
        if (idx != i) {
            swap(arr, i, idx); //交换两个位置的数值
        }
    }
}

3. 插入排序(Insertion Sort)

思路:元素被分为有序区和无序区两部分。最初有序区只有一个元素。每次从无序区中选择一个元素,插入到有序区的位置,直到无序区变空。

与抓扑克牌排序一样。摸第一张时认为已有序,摸第二张时插入到第一张的前或后进行排序,以此类推。
在这里插入图片描述
在这里插入图片描述

/**
 * 最差时间复杂度 ---- 最坏情况为输入序列是降序排列的,此时时间复杂度O(n^2)
 * 最优时间复杂度 ---- 最好情况为输入序列是升序排列的,此时时间复杂度O(n)
 * 平均时间复杂度 ---- O(n^2)
 * 所需辅助空间 ------ O(1)
 * 稳定性 ------------ 稳定
 */
public static void insertionSort(int[] arr) {
    for (int i = 1; i < arr.length; i++) {
        // 从当前i下标往前遍历,插入到相应的位置
        for (int j = i; j > 0; j--) {
        	// 当前数比前面的数小,交换两数
            if (arr[j] < arr[j-1]) {
                swap(arr, j-1, j); //交换两个位置的数值
            } else {
                break;
            }
        }
    }
}
// 插入排序之 右移 与 赋值
public static void insertionSort2(int[] arr) {
    for (int i = 1; i < arr.length; i++) { // 类似抓扑克牌排序
        int get = arr[i]; // 新抓一张牌
        int j = i - 1; // 拿在手上的牌总是排序好的
        while (j >= 0 && arr[j] > get) { // 将抓到的牌与手牌从右向左进行比较
            arr[j + 1] = arr[j]; // 如果该手牌比抓到的牌大,就将其右移
            j--;
        }
        arr[j + 1] = get; // 直到该手牌比抓到的牌小(或二者相等),将抓到的牌插入到该手牌右边(相等元素的相对次序未变,所以插入排序是稳定的)
    }
}

4. 快速排序(Quick Sort)

单向寻找方式

思路:

  1. 从序列中挑出一个元素,作为"基准"(pivot)。从下标为0开始。
  2. 把所有比基准值小的元素放在基准前面,所有比基准值大的元素放在基准的后面(相同的数可以到任一边),这个称为分区(partition)操作。
    当基准值(pivot)取数组第0个,有x个比基准值小的,pivot最终的下标地址p_pos就为x
  3. 对每个分区递归地进行步骤1~2,递归的结束条件是序列的大小是0或1,这时整体已经被排好序了。

在这里插入图片描述

/**
 * 最差时间复杂度 ---- 每次选取的基准都是最大(或最小)的元素,导致每次只划分出了一个分区,需要进行n-1次划分才能结束递归,时间复杂度为O(n^2)
 * 最优时间复杂度 ---- 每次选取的基准都是中位数,这样每次都均匀的划分出两个分区,只需要logn次划分就能结束递归,时间复杂度为O(nlogn)
 * 平均时间复杂度 ---- O(nlogn)
 * 所需辅助空间 ------ 主要是递归造成的栈空间的使用(用来保存left和right等局部变量),取决于递归树的深度,一般为O(logn),最差为O(n)       
 * 稳定性 ---------- 不稳定
 */
public static void quickSort(int[] arr, int left, int right) {
    int pivot; //基准
    int p_pos; //存放基准下标地址:有x个比pivot小的数,p_pos就为x。
    if (left < right) {
        p_pos = left;
        pivot = arr[p_pos]; //基准从数组的第0个开始取
        // 从左往右遍历
        for (int i = left + 1; i <= right; i++) {
            // 当存在 第i个位置 < pivot,交换 p_pos+1 和 i 的数值
            if (arr[i] < pivot){    // > 降序
                p_pos++;
                swap(arr, p_pos, i); //交换两个位置的数值
            }
        }
        // 最终找到 pivot 该去的位置 p_pos,有x个比pivot小的数,p_pos就为x。
        swap(arr, left, p_pos); //交换两个位置的数值

        quickSort(arr, left, p_pos - 1);//递归当前基准 左边 的区域
        quickSort(arr, p_pos + 1, right);//递归当前基准 右边 的区域
    }
}

// 其他实现方式的快速排序
public static void _quickSort(int[] arr, int left, int right) {
    if (left < right){
        int pivot_index = partition(arr, left, right); // 基准的索引
        _quickSort(arr, left, pivot_index - 1);
        _quickSort(arr, pivot_index + 1, right);
    }
}
/**
 * 划分函数/归位函数
 */
public static int partition(int[] arr, int left, int right) {
    int pivot = arr[right], // 选择最后一个作为基准
            tail = left - 1; // tail 为小于基准的子数组的最后一个元素的下标
    for (int i = left; i < right; i++) { // 遍历基准以外的其他元素
        // 把小于等于基准的元素放到前一个子数组末尾
        if (arr[i] <= pivot) {
            swap(arr, ++tail, i); //交换两个位置的数值
        }
    }
    // 最后把基准放到前一个子数组的后边,剩下的子数组既是大于基准的子数组
    // 该操作很有可能把后面元素的稳定性打乱,所以快速排序是不稳定的排序算法
    swap(arr,tail+1, right); //交换两个位置的数值
    return tail + 1; // 返回基准的索引
}

双向寻找方式

思路:

  1. 取arr[0]为基准:pivot=arr[0];
  2. 从后往前遍历,寻找比pivot小的数的下标right,直接arr[0]=arr[right];
  3. 再从前往后遍历:第1位到第right-1位,寻找大于pivot的下标left,直接arr[right]=arr[left];
  4. 再从后往前遍历:第right-1位到第left+1位。。。
  5. 直到left==right,就是基准pivot所在位置。

例如:arr = {5, 7, 4, 6, 3, 1, 2, 9, 8}
arr
a. 取出5
快拿开你的鼠标

b. 从右往左找到比5小的数2,放到5原来的位置
移动2

c. 从左往右找到比5大的数7,放到2原来的位置
移动7

d. 直到左右两边寻找的下标相同,把5放到该位置,则比5小的都在左边,比5大的都在右边
放5

e. 至此形成一个回合,重复递归遍历5左边和右边的区域。

public static void _quickSort(int[] arr,int left, int right) {
    if (left < right) {
        int mid = _partition(arr, left, right);
        _quickSort(arr, left, mid-1);
        _quickSort(arr, mid+1, right);
    }
}
public static int _partition(int[] arr,int left, int right) {
    // 取出第一个
    int pivot = arr[left];
    while (left < right){
        // 从右往左找比pivot小的,放到pivot原来的位置
        while (left < right && arr[right] >= pivot) {
            right--;
        }
        arr[left] = arr[right];
        // 从左往右找比pivot大的,放到上面寻找到的位置
        while (left < right && arr[left] <= pivot) {
            left++;
        }
        arr[right] = arr[left];
    }
    arr[left] = pivot;
    return left;
}

5. 希尔排序(Shell Sort)

算法思想

希尔排序有时又叫做 “缩小间隔排序”,它以插入排序为基础,将原来要排序的列表划分为一些子列表,再对每一个子列表执行插入排序,从而实现对插入排序性能的改进。划分子列的特定方法是希尔排序的关键。
我们并不是将原始列表分成含有连续元素的子列,而是确定一个划分列表的增量 “i”,这个i更准确地说,是划分的间隔。然后把每间隔为i的所有元素选出来组成子列表,然后对每个子序列进行插入排序,最后当 i=1 时,对整体进行一次直接插入排序。

希尔排序是一种分组插入排序算法。
希尔排序每趟并不使某些元素有序,而是使整体数据越来越接近有序;最后一趟排序使得所有数据有序。

思路:

  1. 首先取一个整数d1=len/2,将元素分为d1个组,每组相邻量元素之间距离为d1,在各组内进行直接插入排序;
  2. 取第二个整数d2=d1/2,重复上述分组排序过程,直到di=1,即所有元素在同一组

在这里插入图片描述

public static void shellSort(int[] arr) {
    for (int step = arr.length/2; step > 0 ; step/=2) {
        for (int i = step; i < arr.length; i++) {
            int value = arr[i];
            int j;
            for (j = i-step; j >= 0 && arr[j]>value; j-=step) {
                arr[j+step] = arr[j];
            }
            arr[j+step] = value;
        }
    }
}

6. 归并排序

归并排序是一种递归算法,它持续地将一个列表分成两半。如果列表是空的或者 只有一个元素,那么根据定义,它就被排序好了(最基本的情况)。如果列表里的元素超过一个,我们就把列表拆分,然后分别对两个部分调用递归排序。一旦这两个部分被排序好了,然后就可以对这两部分数列进行归并了。归并是这样一个过程:把两个排序好了的列表结合在一起组合成一个单一的有序的新列表。有自顶向下(递归法)和自底向上的两种实现方法。
在这里插入图片描述

7. 堆排序

8. 基数排序

9. 桶排序

 


若有不正之处,请谅解和批评指正,谢谢~
转载请标明:
https://blog.csdn.net/vihem/article/details/120855183

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值