算法与数据结构-常见排序算法

149 阅读8分钟

常见排序算法

1、冒泡排序
2、插入排序
3、快速排序
4、归并排序

冒泡排序

冒泡过程
给定一个数组,循环遍历。从数组头部开始比较,每两个元素进行比较,一轮结束后,最大或者最小的元素放到了数组的末尾。然后再进行第二轮比较,以此类推,直到完成整个排序过程

代码实现

 public static void sort(int[] numbers) {

        int length = numbers.length;
        for (int i = 0; i < length - 1; i++) {
            for (int j = 0; j < length - 1 - i; j++) {
                if (numbers[j] > numbers[j + 1]) {
                    int temp = numbers[j];
                    numbers[j] = numbers[j + 1];
                    numbers[j + 1] = temp;
                }
            }
        }
    }

插入排序

对数组 [2, 1, 7, 9, 5, 8] 进行插入排序

过程描述

首先将数组分成左右两个部分,左边是已经排好序的部分,右边是还没有排好序的部分,刚开始,左边已排好序的部分只有第一个元素2。接下来,我们对右边的元素一个一个进行处理,将它们放到左边。

先来看 1,由于 1 比 2 小,需要将 1 插入到 2 的前面,做法很简单,两两交换位置即可,[1, 2, 7, 9, 5, 8]。

然后,我们要把 7 插入到左边的部分,由于 7 已经比 2 大了,表明它是目前最大的元素,保持位置不变,[1, 2, 7, 9, 5, 8]。

同理,9 也不需要做位置变动,[1, 2, 7, 9, 5, 8]。

接下来,如何把 5 插入到合适的位置。首先比较 5 和 9,由于 5 比 9 小,两两交换,[1, 2, 7, 5, 9, 8],继续,由于 5 比 7 小,两两交换,[1, 2, 5, 7, 9, 8],最后,由于 5 比 2 大,此轮结束。

最后一个数是 8,由于 8 比 9 小,两两交换,[1, 2, 5, 7, 8, 9],再比较 7 和 8,发现 8 比 7 大,此轮结束。到此,插入排序完毕。

代码如下所示:

public static void insertSort(int[] numbers) {

        int length = numbers.length;
        for (int i = 1; i < length; i++) {
            for (int j = i -1; j >= 0; j--) {
                if (numbers[j + 1] < numbers[j]) {
                    swap(numbers, j, j + 1);
                }
            }
        }
    }
private static void swap(int[] numbers, int i, int j) {
        int temp = numbers[i];
        numbers[i] = numbers[j];
        numbers[j] = temp;
    }

快速排序

基本思想

快速排序也采用了分治的思想。

实现

把原始的数组筛选成较小和较大的两个子数组,然后递归地排序两个子数组。

举例:把班里的所有同学按照高矮顺序排成一排。

解法:老师先随机地挑选了同学 A,让所有其他同学和 A 比高矮,比 A 矮的都站在 A 的左边,比 A 高的都站在 A 的右边。接下来,老师分别从左边和右边的同学里选择了同学 B 和 C,然后不断地筛选和排列下去。

在分成较小和较大的两个子数组过程中,如何选定一个基准值(也就是同学 A、B、C 等)尤为关键。

例题分析

对数组 [2, 1, 7, 9, 5, 8] 进行排序。

解题思路

按照快速排序的思想,首先把数组筛选成较小和较大的两个子数组。

随机从数组里选取一个数作为基准值,比如 7,于是原始的数组就被分成了两个子数组。注意:快速排序是直接在原始数组里进行各种交换操作,所以当子数组被分割出来的时候,原始数组里的排列也被改变了。

接下来,在较小的子数组里选 2 作为基准值,在较大的子数组里选 8 作为基准值,继续分割子数组。

继续将元素个数大于 1 的子数组进行划分,当所有子数组里的元素个数都为 1 的时候,原始数组也被排好序了。

代码示例

主体函数代码如下。



void sort(int[] nums, int lo, int hi) {
    if (lo >= hi) return; // 判断是否只剩下一个元素,是,则直接返回
    
    // 利用 partition 函数找到一个随机的基准点
    int p = partition(nums, lo, hi);
    
    // 递归地对基准点左半边和右半边的数进行排序
    sort(nums, lo, p - 1);
    sort(nums, p + 1, hi);
}

下面用代码实现 partition 函数获得基准值。


int partition(int[] nums, int lo, int hi) {
    // 随机选择一个数作为基准值,nums[hi] 就是基准值
    swap(nums, randRange(lo, hi), hi);

    int i, j;

    // 从左到右用每个数和基准值比较,若比基准值小,则放到指针 i 所指向的位置。循环完毕后,i 指针之前的数都比基准值小
    for (i = lo, j = lo; j < hi; j++) {
        if (nums[j] <= nums[hi]) {
            swap(nums, i++, j);
        }
    }

    // 末尾的基准值放置到指针 i 的位置,i 指针之后的数都比基准值大
    swap(nums, i, j);

    //返回指针i,作为基准点的位置
    return i;
}

算法分析 时间复杂度

  1. 最优情况:被选出来的基准值都是当前子数组的中间数。

这样的分割,能保证对于一个规模大小为 n 的问题,能被均匀分解成两个规模大小为 n/2 的子问题(归并排序也采用了相同的划分方法),时间复杂度就是:T(n) = 2×T(n/2) + O(n)。

把规模大小为 n 的问题分解成 n/2 的两个子问题时,和基准值进行了 n-1 次比较,复杂度就是 O(n)。很显然,在最优情况下,快速排序的复杂度也是O(nlogn)。

  1. 最坏情况:基准值选择了子数组里的最大或者最小值

每次都把子数组分成了两个更小的子数组,其中一个的长度为 1,另外一个的长度只比原子数组少 1。

举例:对于数组来说,每次挑选的基准值分别是 9、8、7、5、2。

解法:划分过程和冒泡排序的过程类似。

算法复杂度为O(n2)。

提示:可以通过随机地选取基准值来避免出现最坏的情况。

空间复杂度

和归并排序不同,快速排序在每次递归的过程中,只需要开辟 O(1) 的存储空间来完成交换操作实现直接对数组的修改,又因为递归次数为 logn,所以它的整体空间复杂度完全取决于压堆栈的次数,因此它的空间复杂度是 O(logn)。

每次随机选取一个基准值,将数组分成较小的一半和较大的一半,然后检查这个基准值最后所在的下标是不是 k,算法复杂度只需要 O(n)。

归并排序

基本思想

核心是分治,就是把一个复杂的问题分成两个或者多个相似的子问题,然后把子问题分成更小的子问题,直到子问题可以简单的直接求解,原始问题的解就是子问题解的合并。

实现

一开始先把数组从中间划分成两个子数组,一直递归地把子数组划分成更小的数组,直到子数组中只有一个元素,才开始排序

排序的方法就是按照大小顺序合并两个元素,接着以此按照递归的返回顺序,不断得合并排好序的子数组,直到最后把整个数组的顺序排号。

private static void sort(int[] A, int lo, int hi) {
        //判断是否只剩下一个元素
        if (lo >= hi) {
            return;
        }
        //从中间将数组分成两个部分
        int mid = lo + (hi - lo) / 2;
        sort(A, lo, mid);
        sort(A, mid + 1, hi);
        marge(A, lo, mid, hi);
    }
    
private static void marge(int[] nums, int lo, int mid, int hi) {
        //复制一份原来的的数组
        int[] copy = nums.clone();
        //定义一个 k 指针表示从什么位置开始修改原来的数组,i 指针表示左半边的起始位置,j 表示右半边的起始位置
        int k = lo, i = lo, j = mid + 1;

        while (k <= hi) {
            if (i > mid) {
                nums[k++] = copy[j++];
            } else if (j > hi) {
                nums[k++] = copy[i++];
            } else if (copy[j] < copy[i]) {
                nums[k++] = copy[j++];
            } else {
                nums[k++] = copy[i++];
            }
        }
    }

其中,While 语句比较,一共可能会出现四种情况。

左半边的数都处理完毕,只剩下右半边的数,只需要将右半边的数逐个拷贝过去。

右半边的数都处理完毕,只剩下左半边的数,只需要将左半边的数逐个拷贝过去就好。

右边的数小于左边的数,将右边的数拷贝到合适的位置,j 指针往前移动一位。

左边的数小于右边的数,将左边的数拷贝到合适的位置,i 指针往前移动一位。

本文内容来自拉勾教育《300分钟搞定数据结构与算法》 kaiwu.lagou.com/course/cour…