面试代码——排序算法【建议收藏】

最近家里小朋友准备计算机类的研究生复试,可能会考到常见的排序算法,于是帮助整理一波,顺带复习下相关知识,考验下自己的编码能力;

关于排序算法,网上关于排序算法的帖子和代码也比较多,有的帖子甚至连排序过程的动图都有,但是还存在一个问题——到了代码的实现层面,讲述的并不清楚,甚至没有一行代码注释,不便于理解和记忆,因此本文整理下相关代码并附上尽量详细的注释,便于理解和记忆;

代码的优雅不仅在于简洁,实际生产中更重要的是可读性和维护性;

以下按照算法的难易程度分别介绍;

1. 冒泡排序

冒泡排序(Bubble Sort)也可以称之为"交换排序";

  • 冒泡排序需要执行N轮排序;每轮待排序序列长度为N、N-1...1;
  • 每次排序后,将最大元素通过[交换]移动到了序列的尾部;则下一轮未排序的序列长度就减少1个;
  • 对于单轮排序,从头到尾遍历元素,比较当前元素以及其相邻元素,将更大的元素通过[交换]操作移到后面;
  • 选择排序与冒泡排序很像,存在一点区别,即选择排序每轮排序仅需要找"剩余元素序列"的最大值的位置,将最值交换到序列尾部,即选择排序每轮只"交换1次";
  • 而冒泡排序的一轮遍历可能发生多次交换操作,冒泡排序虽然交换次数多了,但是如果某一轮排序中未发生任何元素交换,则说明剩余序列已经有序,无需继续排序

时间复杂度:O(n^2)

package sort.popup;

import java.util.Arrays;
import java.util.List;

/**
 * 冒泡排序(交换排序):每次冒泡,通过相邻元素的依次比较和[交换],就把当前"还未排序"的序列最大值放到尾部,下次未排序的序列长度就减少1个;
 * 区别于选择排序,选择排序每次仅需要找"剩余元素序列"的最值,交换到序列尾部,即只交换1次;
 * 而冒泡排序的一次遍历可能发生多次交换操作,冒泡排序虽然交换次数多了,但是如果某一次交换操作中未发生任何元素交换,则说明已经有序,无需继续排序;
 * 比较次数:n,n-1,...1 ,复杂度 O(n^2)
 */
public class PopupSort {

    public static int[] initData() {
        List<Integer> input = Arrays.asList(8, 3, 9, 7, 7, 1, 9, 9, 5);
        int[] array = new int[input.size()];
        for (int i = 0; i < input.size(); i++) {
            array[i] = input.get(i);
        }
        System.out.println("input:" + Arrays.toString(array));
        return array;
    }

    public static void swap(int[] array, int i, int j) {
        int temp = array[i];
        array[i] = array[j];
        array[j] = temp;
    }

    // 从小到大顺序
    // 第i次冒泡操作:从0往后,依次和相邻元素比较,若当前元素更则大则交换,然后继续往后比较,直到size-i的位置;
    // 通过一次冒泡,当前这次操作就把最大的元素放到了序列尾部;
    // 注意:如果某一次交换操作中未发生任何元素交换,则说明已经有序;
    public static void sort(int[] src) {
        for (int i = 0; i < src.length; i++) {
            boolean sorted = true;
            for (int j = 0; j + 1 < src.length - i; j++) {
                if (src[j] > src[j + 1]) {
                    swap(src, j, j + 1);
                    sorted = false;
                }
            }
            if (sorted) {
                break;
            }
        }
    }

    public static void main(String[] args) {
        int[] array = initData();
        sort(array);
        System.out.println("sorted:" + Arrays.toString(array));
    }

}

动图

 2. 选择排序

  • 选择排序与冒泡排序非常相似;
  • 选择排序需要执行N轮排序;每轮待排序序列长度为N、N-1...1;
  • 每次排序后,将最大元素通过[交换]移动到了序列的尾部;则下一轮未排序的序列长度就减少1个;
  • 对于单轮排序,从头到尾遍历元素,记录当前待排序序列中的最值元素的位置,将最值元素位置与当前序列最后元素做1次[交换],移到序列尾部;
  • 区别于冒泡排序,尽管每一轮的交换操作次数固定为1次,但选择排序遍历元素的次数是固定的;而冒泡排序在某次操作未发生元素交换时,就说明已经有序,无需继续排序;

时间复杂度:O(n^2)

package sort.choose;

import java.util.Arrays;
import java.util.List;

/**
 * 选择排序:每次找"剩余元素序列"的最值,放到序列尾部,即每次遍历只发生一次交换操作(这点区别于冒泡排序);选择排序也是最直观最好理解的排序;
 * 区别于冒泡排序,选择排序遍历元素的次数是固定的;而冒泡排序在某次操作未发生元素交换时,就说明已经有序,无需继续排序;
 * 比较次数:n,n-1,...1 ,复杂度 O(n^2)
 */
public class ChooseSort {

    public static int[] initData() {
        List<Integer> input = Arrays.asList(8, 3, 9, 7, 7, 1, 9, 9, 5);
        int[] array = new int[input.size()];
        for (int i = 0; i < input.size(); i++) {
            array[i] = input.get(i);
        }
        System.out.println("input:" + Arrays.toString(array));
        return array;
    }

    public static void swap(int[] array, int i, int j) {
        int temp = array[i];
        array[i] = array[j];
        array[j] = temp;
    }

    // 从小到大顺序
    // 第i次操作:从0往后遍历"剩余元素序列",直到size-i的位置,找到最大的元素,放在当前序列的尾部;
    public static void sort(int[] src) {
        for (int i = 0; i < src.length; i++) {
            int maxIndex = 0;
            int lastIndex = src.length - i - 1;
            for (int j = 1; j <= lastIndex; j++) {
                if (src[j] > src[maxIndex]) {
                    maxIndex = j;
                }
            }
            swap(src, maxIndex, lastIndex);
        }
    }

    public static void main(String[] args) {
        int[] array = initData();
        sort(array);
        System.out.println("sorted:" + Arrays.toString(array));
    }
}

动图

3. 插入排序

  • 插入排序也比较好理解,只要打过扑克牌的人都应该能够秒懂;就像打斗地主取牌一样,手牌已经有序,每次取到新排,根据大小关系插入到手牌中合适的位置;
  • 从第1个元素开始,逐渐构建有序序列,遍历未排序的元素,逐个插入这个有序序列;有序序列长度从1、2...到N,待排序序列长度从N-1、N-2...到0;
  • 元素逐个插入这个有序序列:记录一个当前位置pos,从后往前遍历有序元素序列,若大于该元素,则把pos更新到该元素位置,然后把当该元素往后"挤"1位(通过赋值或交换来实现都可以);
  • 每一轮比较次数最大值:1,2,...,n-1;
  • 插入排序的原理导致他天然的存在一个优点:插入排序在对几乎已经排好序的数据操作时,几乎不需要怎么重新插入,效率高

时间复杂度:O(n^2)

package sort.insert;

import java.util.Arrays;
import java.util.List;

/**
 * 插入排序:从第1个元素开始,逐渐构建有序序列,未排序的元素插入这个有序序列;
 * 理解:就像打斗地主取牌一样,手牌已经有序,每次取到新排,根据大小关系插入手牌中合适的位置;
 * 比较次数:1,2,...,n-1,n,复杂度 O(n^2)
 * 特点:插入排序在对几乎已经排好序的数据操作时,几乎不需要怎么重新插入,效率高
 */
public class InsertSort {

    public static int[] initData() {
        List<Integer> input = Arrays.asList(8, 3, 9, 7, 7, 1, 9, 9, 5);
        int[] array = new int[input.size()];
        for (int i = 0; i < input.size(); i++) {
            array[i] = input.get(i);
        }
        System.out.println("input:" + Arrays.toString(array));
        return array;
    }

    public static void swap(int[] array, int i, int j) {
        int temp = array[i];
        array[i] = array[j];
        array[j] = temp;
    }

    // 从小到大顺序
    // 从1开始,第i次操作时,前i个元素已经有序,将第i+1个元素从后往前/从前往后依次与已经有序的序列比较,找到插入"正确的位置";直到i+1是最后一个数组元素,完成最后一次操作;
    // 插入"正确的位置":若以满足大小关系则跳出,否则与当前元素交换,临时存放到交换元素的原位置上,继续往前比较
    public static void sort(int[] src) {
        for (int i = 1; i < src.length; i++) {
            int current = src[i];
            int pos = i;
            // 找到插入的pos
            for (int j = i - 1; j >= 0; j--) {
                if (current > src[j]) {
                    break;
                } else {
                    // 后移j元素 更新最终要放入的位置pos 当然这里也可以用swap来做移位,就不需要更新pos了
                    src[j + 1] = src[j];
                    // 考虑到方法在跳出后要把元素放入pos位置 因此需要在跳出前 每次比较后都及时更新当前pos
                    pos = j;
                }
            }
            // 插入到pos位置
            src[pos] = current;
        }
    }

    public static void main(String[] args) {
        int[] array = initData();
        sort(array);
        System.out.println("sorted:" + Arrays.toString(array));
    }

}

动图

4. 堆排序

堆排序可以参考我的这篇文章:编码踩坑——MySQL order by&limit顺序不一致 / 堆排序 / 排序稳定性

  • 堆排序利用堆数据结构的特点:完全二叉树&堆顶元素最大/最小,先对待排序序列构建堆,再依次从堆顶取出元素即完成排序;
  • 堆排序本身没有元素值比较过程,而是在构建堆/堆化过程中完成比较;
  • 堆排序的过程:遍历待排序元素,依次入堆,然后依次从堆顶取出元素,每取一次,都需要重新[堆化]以维护堆的数据结构;
  • 由于每次从堆顶取出元素后,会把堆底元素交换到堆顶,因此排序是不稳定的,即可能改变值相等元素的初始相对位置;

树深度log2(N),复杂度 O(N*log2(N))

package sort.heap;

import java.util.Arrays;
import java.util.List;

/**
 * 堆排序:利用堆数据结构的特点:完全二叉树+对顶元素最大/最小,先构建堆再依次从堆顶取出元素即完成排序
 * 树深度log2(N),复杂度 O(n*log2(n))
 */
public class HeapSort {

    public static int[] initData() {
        List<Integer> input = Arrays.asList(8, 3, 9, 7, 7, 1, 9, 9, 5);
        int[] array = new int[input.size()];
        for (int i = 0; i < input.size(); i++) {
            array[i] = input.get(i);
        }
        System.out.println("input:" + Arrays.toString(array));
        return array;
    }

    public static void swap(int[] array, int i, int j) {
        int temp = array[i];
        array[i] = array[j];
        array[j] = temp;
    }

    public static void main(String[] args) {
        int[] array = initData();
        sort(array);
        System.out.println("output:" + Arrays.toString(array));
    }

    public static void sort(int[] array) {
        // 初始化堆,插入全部元素
        BigHeap bigHeap = new BigHeap(array.length);
        for (int i = 0; i < array.length; i++) {
            bigHeap.insert(array[i]);
        }
        // 堆顶元素最大,依次从堆顶取出元素插入数组序列完成排序
        for (int i = array.length - 1; i >= 0; i--) {
            array[i] = bigHeap.remove();
        }
    }

    // 从上到下执行堆化 无需交换元素则退出 父节点下标i,左节点若存在则下标为i*2,右节点若存在则下标为i*2+1
    public static void heapAsc(int[] array, int i, int count) {
        while (true) {
            int maxIndex = i;
            if (i * 2 <= count && array[i * 2] > array[maxIndex]) {
                maxIndex = i * 2;
            }
            if (i * 2 + 1 <= count && array[i * 2 + 1] > array[maxIndex]) {
                maxIndex = i * 2 + 1;
            }
            if (maxIndex == i) {
                break;
            }
            swap(array, maxIndex, i);
            i = maxIndex;
        }
    }

    // 从下到上执行堆化 无需交换元素则退出 子节点下标i,父节点若存在则下标为i/2
    public static void heapDesc(int[] array, int i) {
        while (true) {
            int maxIndex = i;
            if (i / 2 >= 1 && array[i] > array[i / 2]) {
                swap(array, i, i / 2);
                i = i / 2;
            } else {
                break;
            }
        }
    }
}

// 大顶堆 最大的元素在上面
class BigHeap {
    // 元素序列
    int[] array;
    // 当前堆元素数量 由于数组下标0不放元素 因此最大为数组size-1
    int count;

    // 初始化堆大小 最多放elemNum个元素
    public BigHeap(int elemNum) {
        array = new int[elemNum + 1];
        count = 0;
    }

    // 堆顶移除元素 将堆尾部元素换到堆顶 再从上而下的堆化(最小子树有序)
    public int remove() {
        if (count <= 0) {
            throw new RuntimeException("already empty");
        }
        int max = array[1];
        HeapSort.swap(array, 1, count);
        count--;
        HeapSort.heapAsc(array, 1, count);
        return max;
    }

    // 插入新元素到堆尾 从下到上的执行堆化 只需要根父节点比较大小即可
    public void insert(int item) {
        if (count == array.length - 1) {
            throw new RuntimeException("already empty");
        }
        count++;
        array[count] = item;
        HeapSort.heapDesc(array, count);
    }

}

动图

 5. 希尔排序

  • 希尔排序(Shell Sort)是插入排序的一种,也称缩小增量排序(Diminishing Increment Sort);
  • 希尔排序是基于插入排序的改进:
  1. 充分利用插入排序 "在对几乎已经排好序的数据操作时,几乎不需要怎么重新插入,效率高" 的特点;
  2. 插入排序相对有序的效率略低,因为每次交换只能移动1位,而shell排序每次可以移动增量长度的位置;
  • 希尔排序过程:设置一个增量step,将原序列分为step个小段(各个小段的起始元素下标分别为0~step-1),对每个小段执行插入排序,这样就把较小的值尽量的移到序列前面;
  • 然后逐渐减小step,一般step=step/2,每个小段的元素数量越来越多,直到N=1则小段退化成整个序列,此时序列基本上已经相对有序了,最后再做一次全序列的插入排序;
  • 关于排序稳定性,由于希尔排序中需要多次插入排序,我们知道一次插入排序是稳定的,不会改变相同元素的相对顺序;但在不同小段的插入排序过程中,相同的元素可能分布在不同小段,在各自小段的插入排序中移动,可能有的在小段中移动到前面,有的可能未发生移动,导致相同元素的初始相对顺序被破坏,最后其稳定性就会被打乱,所以shell排序是不稳定的
package sort.shell;

import java.util.Arrays;
import java.util.List;

/**
 * 希尔排序:设置一个增量N,将原序列分为N个小段,对每个小段执行插入排序,这样就把较小的值尽量的移到序列前面;然后逐渐减小N,一般N=N/2,直到N=1,此时序列基本上已经相对有序了,最后再做一次全序列的插入排序;
 * 希尔排序是基于插入排序的改进,充分利用插入排序 "在对几乎已经排好序的数据操作时,几乎不需要怎么重新插入,效率高" 的特点
 *
 * @link https://blog.csdn.net/yuan2019035055/article/details/120246584?ops_request_misc=&request_id=&biz_id=102&utm_term=%E5%B8%8C%E5%B0%94%E6%8E%92%E5%BA%8F&utm_medium=distribute.pc_search_result.none-task-blog-2~all~sobaiduweb~default-0-120246584.nonecase&spm=1018.2226.3001.4449
 */
public class ShellSort {

    public static int[] initData() {
        List<Integer> input = Arrays.asList(8, 3, 9, 7, 7, 1, 9, 9, 5);
        int[] array = new int[input.size()];
        for (int i = 0; i < input.size(); i++) {
            array[i] = input.get(i);
        }
        System.out.println("input:" + Arrays.toString(array));
        return array;
    }

    public static void swap(int[] array, int i, int j) {
        int temp = array[i];
        array[i] = array[j];
        array[j] = temp;
    }

    // 带步长的插入排序: 先写步长为1的插入排序 再把步长1该为step
    public static void insertSort(int[] src, int start, int step) {
        // 插入时 第一个元素作为有序列 从第一个元素的下一个元素开始 往前插入
        for (int i = (start + step); i < src.length; i = i + step) {
            int val = src[i];
            int pos = i;
            for (int j = i - step; j >= 0; j = j - step) {
                if (src[j] < val) {
                    break;
                } else {
                    src[j + step] = src[j];
                    pos = j;
                }
            }
            src[pos] = val;
        }
    }

    // 从小到大顺序
    // 每个小段的元素数量从2开始,则分成了N/2个小组:(0,N/2)、(1,N/2+1)...(N/2-1,N),步长step初始值为N/2
    // 每一轮中,依次对每个小段内执行插入排序,一轮排完后,整体序列相对有序;
    // 下一轮,减小step(一般step=step/2),每组内元素越来越多,直到step=1,则只剩下一组,对整个相对有序的序列执行插入排序
    // 随着轮次迭代,整个序列的相对有序程度越来越大,插入排序成本越来越小;
    public static void sort(int[] src) {
        for (int step = src.length / 2; step >= 1; step = step / 2) {
            for (int start = 0; start < step; start++) {
                insertSort(src, start, step);
            }
            System.out.println("step:" + step + " 当前轮次排序结果:" + Arrays.toString(src));
        }
    }

    public static void main(String[] args) {
        int[] array = initData();
        sort(array);
        System.out.println("sorted:" + Arrays.toString(array));
    }
}

示意图

增量为4时,

6. 归并排序

  • 归并排序(Merge sort)是建立在归并操作上的一种有效的排序算法;该算法是采用分治法(Divide and Conquer)的一个非常典型的应用;
  • 归并排序的思路:将当前序列分为(left,mid)和(mid+1,left)2段,分别对其执行排序;分别有序后,再执行"合并2个有序序列"的操作;
  • 最容易理解的实现方式就是递归写法;当然,所有的递归都可以用循环改写,本质是分组排序,每个组内元素数量从1到(length)/2;
  • 归并的空间复杂度就是那个临时的数组和递归时压入栈的数据占用的空间:N+logN;所以空间复杂度为: O(n);
  • 归并排序的时间复杂度是O(NlogN);
package sort.merge;

import java.util.Arrays;
import java.util.List;

/**
 * 归并排序:将当前序列分为(left,mid)和(mid+1,left)2段,分别对其执行排序;分别有序后,再执行"合并2个有序序列";
 * 最容易理解的实现方式就是递归写法;当然,所有的递归都可以用循环改写,本质是分组排序,步长从1到(length+1)/2;
 * 归并的空间复杂度就是那个临时的数组和递归时压入栈的数据占用的空间:n + logn;所以空间复杂度为: O(n)
 * 归并排序的时间复杂度是O(nlogn)
 * 参考:https://blog.csdn.net/qq_52777887/article/details/123858393
 */
public class MergeSort {

    public static int[] initData() {
        List<Integer> input = Arrays.asList(8, 3, 9, 7, 7, 1, 9, 9, 5);
        int[] array = new int[input.size()];
        for (int i = 0; i < input.size(); i++) {
            array[i] = input.get(i);
        }
        System.out.println("input:" + Arrays.toString(array));
        return array;
    }

    // (1)递归实现:先将大序列拆分为2个子序列,对2个子序列继续执行当前排序方法(递归直到子序列长度为2),最后合并2个有序序列
    public static void sort(int[] src, int left, int right) {
        if (left >= right) {
            return;
        }
        int mid = (left + right) / 2;
        sort(src, left, mid);
        sort(src, mid + 1, right);
        merge(src, left, mid, right);
    }

    // 核心方法:合并2个有序序列
    private static void merge(int[] src, int left, int mid, int right) {
        int[] temp = new int[right - left + 1];
        int t = 0;
        // 左右子序列的当前数组下标
        int l = left;
        int r = mid + 1;

        // 1. 当2个有序数组都没有取完时
        while (l <= mid && r <= right) {
            if (src[l] < src[r]) {
                temp[t] = src[l];
                t++;
                l++;
            } else {
                temp[t] = src[r];
                t++;
                r++;
            }
        }
        // 2. 左序列还没取完 右序列取完了 直接往temp后面加
        while (l <= mid) {
            temp[t] = src[l];
            t++;
            l++;
        }
        // 3. 右序列还没取完 左序列取完了 直接往temp后面加
        while (r <= right) {
            temp[t] = src[r];
            t++;
            r++;
        }

        // 临时数组的结果设置到原序列上
        for (int i = 0; i < temp.length; i++) {
            src[left + i] = temp[i];
        }
    }

    // (2)循环实现:步长从1到N/2,即子序列元素数从2到length
    // 步长为1时,组别划分:[(0),(1)]、[(2),(3)]、...[(N-2),(N-1)],子序列最大长度为1
    // 步长为2时,组别划分:[(0,1),(2)]、[(3,4),(5)]、...[(N-2,N-2),(N-1)],子序列最大长度为2
    // 步长为N时,组别划分:[(0,1,...,N/2),(N/2+1,...N-2,N-1)],子序列最大长度为N/2,合并完即完成全部排序
    public static void sortV2(int[] src) {
        for (int step = 1; step <= src.length; step = step * 2) {
            int left = 0;
            int right = left + step;
            int mid = (left + right) / 2;
            while (left < right && right < src.length) {
                merge(src, left, mid, right);
                // 下一组的起始位置 right + 1
                left = right + 1;
                // 下一组的终点 新left+step
                right = left + step;
                mid = (left + right) / 2;
            }
        }
    }

    public static void main(String[] args) {
        int[] array = initData();
        sort(array, 0, array.length - 1);
        System.out.println("sorted:" + Arrays.toString(array));

        int[] arrayV2 = initData();
        sortV2(arrayV2);
        System.out.println("sortedV2:" + Arrays.toString(arrayV2));
    }
}

动图

7. 快速排序

  • 快速排序是一种分治思想的典型应用;
  • 快速排序的思路:对于整个排序序列,取一个值V(如序列中的最左元素),然后通过"交换"操作移位,将该元素放到序列的位置pos,以满足——pos左侧元素都小于V,pos右侧元素都大于等于V;这样,经过一次排序,当前元素在整个序列的位置就"确定好了"
  • 随后,分别对于该元素的左侧序列和右侧序列,继续上述操作,直到子序列长度"小于等于1";
  • 在排序的过程中,子序列长度会越来越小,因此交换次数也会越来越小;
  • 快速排序的时间复杂度是O(nlogn);

对快速排序的优化思路

(1) 当元素数量较小,快速排序优势小,可以使用其他的排序如插入排序;

(2) V的取值尽量为当前序列的平均值,这样子序列就被均匀分开,移动次数也相对较小;

快速排序的核心方法就是找划分序列位置的方法,这里推荐两种比较好理解的方法:

(1)构建左序列方法(小于基准值V),基准值换到start位置,从start+1位置开始逐渐构建左序列,需要遍历完整个序列,把小于V的值依次"交换"到start+1、start+2、...位置,最后再做一次V所在的位置start与左序列最后元素的交换,以满足 左 < V <= 右,返回V的位置;

(2)hoare方法,分别设p、q两个指针分别从头start+1、从尾end遍历序列元素;对于p,若不满足小于基准值v则p停下否则往后移动;类似的对于q,若不满足大于等于基准值v则q停下否则往前移动;当p、q都停下时,交换p、q位置元素;重复这个过程直到p、q相遇(位置相等),然后再做一次V所在的位置start与左序列最后元素的交换(这个元素位置可能是p,也可能是p-1),最后返回p或q(p=q)即可;

package sort.fast;

import java.util.Arrays;
import java.util.List;

/**
 * 快速排序:是一种分治思想的典型应用;对于整个排序序列,取一个值V(如序列中的最左元素),然后通过"交换"操作移位,将该元素放到序列的位置pos,
 * 以满足——pos左侧元素都小于V,pos右侧元素都大于等于V;
 * 这样,经过一次排序,当前元素在整个序列的位置就"确定好了";
 * 随后,分别对于左侧序列和右侧序列,继续上述操作,直到子序列长度"小于等于1";
 * 在排序的过程中,整个序列会逐渐相对有序,因此交换次数也会越来越小;
 * 快速排序的时间复杂度是O(nlogn)
 * 对快速排序的优化思路:
 * (1)当元素数量较小,快速排序优势小,可以使用其他的排序如插入排序;
 * (2)V的取值尽量为当前序列的平均值,这样子序列就被均匀分开,移动次数也相对较小,简化计算则可以使用左/中/右三数取中位数的方式;
 * refer:https://blog.csdn.net/LiangXiay/article/details/121421920
 */
public class FastSort {

    public static int[] initData() {
        List<Integer> input = Arrays.asList(8, 3, 9, 7, 7, 1, 9, 9, 5);
        int[] array = new int[input.size()];
        for (int i = 0; i < input.size(); i++) {
            array[i] = input.get(i);
        }
        System.out.println("input:" + Arrays.toString(array));
        return array;
    }

    public static void swap(int[] array, int i, int j) {
        int temp = array[i];
        array[i] = array[j];
        array[j] = temp;
    }

    // 从小到大顺序
    // (1)找到pos(2)对左右序列分别做上述操作
    public static void quickSort(int[] src, int start, int end) {
        // 退出条件
        if (start >= end) {
            return;
        }
        // 找到V
        int pos = partitionV2(src, start, end);
        quickSort(src, start, pos - 1);
        quickSort(src, pos + 1, end);
    }

    // 核心方法:
    // 1.找合适的V,为尽量打散序列顺序,且为了好挪元素,尽可能取左中右三个值中平均的那个值作为基准值V,然后跟start位置元素做交换
    // 关于找V的位置并调整序列元素顺序以满足——pos左侧元素都小于V,pos右侧元素都大于V,有几种方法,这里使用最好理解的 "仅遍历一次序列的方法"
    // 算法描述:pos初始化为0,假设从1开始遍历序列,找到小于V的元素位置时,则换到(pos+1)位置,然后往前移动pos(即pos++);
    // 当遍历结束时,1到pos位置的元素都小于V,再将位置0与pos位置元素互换,则整个序列满足pos位置之前的元素都小于V(src[pos]),之后的元素都不小于V;
    // 正是这一次的交换操作,导致"快速排序是不稳定的",类似堆排序中的堆尾元素换到堆顶;
    private static int partition(int[] src, int start, int end) {
        // 1. 把相对平均的值换到序列起始位置 作为基准值
        int mid = (start + end) / 2;
        if (src[start] < src[end]) {
            if (src[mid] > src[end]) {
                swap(src, start, end);
            } else {
                swap(src, start, mid);
            }
        } else {
            if (src[mid] > src[end]) {
                swap(src, start, mid);
            } else {
                swap(src, start, end);
            }
        }
        int baseValue = src[start];
        int pos = start;

        // 2. 从start+1位置遍历序列
        for (int i = start + 1; i <= end; i++) {
            // 小于基准值 则挪动pos 把i位置元素换到前面的pos位置去
            if (src[i] < baseValue) {
                pos++;
                swap(src, i, pos);
            }
        }

        // 3. 此时 start+1到pos位置的元素都小于V 最后将start位置的基准值V元素与pos位置元素交换 以满足基准值V在序列中的位置把序列按大小关系划分为2个子序列:左 < V <= 右
        swap(src, start, pos);
        return pos;
    }

    // 另一种比较好理解的找V的位置方法的实现——hoare法
    private static int partitionV2(int[] src, int start, int end) {
        int baseVal = src[start];
        int left = start + 1;
        int right = end;
        while (left < right) {
            // 从左到右遍历序列 满足大小关系则往右移动left
            while (left < right && src[left] < baseVal) {
                left++;
            }
            // 从右到左遍历序列 满足大小关系则往左移动right
            while (left < right && src[right] >= baseVal) {
                right--;
            }
            // 此时left位置元素大于等于baseVal right位置元素小于baseVal 则交换这两个位置的元素后重新满足了大小关系; 然后继续循环 直到2个指针相遇
            if (left < right) {
                swap(src, left, right);
            }
        }
        // 此时left与right相遇(left=right) 但注意此时left位置元素与val大小关系未知!
        // 1.因为可能执行left++后,left位置元素大于等于于val,但刚好左右指针相遇,不再移动right;即此时left位置元素大于等于val
        // 2.但是还有可能执行left++后,left位置元素小于val,然后再执行right的移动时,right++后,左右指针相遇,最终跳出;即此时left位置元素小于val
        // 所以这里需要根据相遇位置元素val的大小关系来判断如何做交换:若src[left]<=val则交换,否则left-1必然小于val,直接与left-1位置交换;
        if (src[left] <= baseVal) {
            swap(src, start, left);
        } else {
            swap(src, start, left - 1);
        }
        return right;
    }

    public static void main(String[] args) {
        int[] array = initData();
        quickSort(array, 0, array.length - 1);
        System.out.println("quickSorted:" + Arrays.toString(array));
    }
}

动图

8. 计数排序

  • 计数排序的核心思想是空间换时间;
  • 计数排序实际上根本没有序列元素的比较过程,而是按照计数数组存放排序序列元素,数组下标为排序元素值,数组下标的值为"值等于数组下标的元素的个数",最后只需要按序遍历计数数组输出排序后的序列即可;

计数排序步骤:

(1)构建计数数组

遍历排序序列,当前元素值记为X,然后找到计数数组的X位置,对于arr[x]++;如此,计数数组顾名思义,记录了每个排序元素的在排序序列中的出现次数;排序元素值为计数数组的下标(计数数组下标天然的满足自然数有序!);

(2)遍历计数数组

初始化好计数数组后,只需要遍历该数组,依次取出数组元素大于1的数组下标值,则可以输出"排好序"的元素序列;

package sort.count;

import java.util.Arrays;
import java.util.List;

/**
 * 计数排序的核心思想是空间换时间,实际上根本没有序列元素的比较过程,而是按照计数数组存放排序序列元素,数组下标为排序元素值,数组下标的值为"值等于数组下标的元素的个数",最后只需要按序遍历计数数组输出排序后的序列即可;
 * 计数排序步骤:遍历排序序列,当前元素值记为X,然后找到计数数组的X位置,对于arr[x]++;
 * 如此,计数数组顾名思义,记录了每个排序元素的在排序序列中的出现次数;排序元素值为计数数组的下标(计数数组下标天然的满足自然数有序!);
 * 初始化好计数数组后,只需要遍历该数组,则可以输出"排好序"的元素序列;
 * refer:https://blog.csdn.net/justidle/article/details/104203972
 */
public class CountSort {

    public static int[] initData() {
        List<Integer> input = Arrays.asList(8, 3, 9, 7, 7, 1, 9, 9, 5);
        int[] array = new int[input.size()];
        for (int i = 0; i < input.size(); i++) {
            array[i] = input.get(i);
        }
        System.out.println("input:" + Arrays.toString(array));
        return array;
    }

    // 从小到大顺序
    // (1)先初始化计数数组,数组长度为(最大值-最小值+1),对于正整数序列,则仅找最大值即可(就不去找最小值了,可能会多申请一些数组空间),最小值按0处理(如果排序序列存在负数,则需要一个相对值将其改写为正数数序列)
    // (2)遍历计数数组,按数组下标顺序输出存储的序列元素,对计数为0的跳过,对计数>1的每输出一次则计数减少1,直到遍历完整个计数数组
    public static void sort(int[] src) {
        int gap = getMax(src) - 0;
        int[] arr = new int[gap + 1];
        // 初始化数计数数组
        for (int i = 0; i < src.length; i++) {
            int item = src[i];
            arr[item]++;
        }
        // 遍历计数数组 输出有序的元素序列
        int index = 0;
        for (int i = 0; i < arr.length; i++) {
            while (arr[i] > 0) {
                src[index++] = i;
                arr[i]--;
            }
        }
    }

    private static int getMax(int[] src) {
        int max = src[0];
        for (int i = 1; i < src.length; i++) {
            if (src[i] > max) {
                max = src[i];
            }
        }
        return max;
    }

    public static void main(String[] args) {
        int[] array = initData();
        sort(array);
        System.out.println("sorted:" + Arrays.toString(array));
    }

}

动图

9. 基数排序

  • 基数排序是一种特殊的排序方式,不是基于比较进行排序,而是基于排序元素值的各个数位大小进行"分配"和"收集",本质属于桶排序(分组排序)的一种实现方式;
  • 基数排序的思路就是将排序序列元素按照元素特征拆成多个组(在基数k下分为多个数位),分别对每个组做类似计数排序的"收集"(收集数组的长度为k,数组下标即当前元素在该数位下的特征值),最后遍历收集数组,输出当数位下的有序序列;
  • 对下一数位排序时,以上一数位的输出作为输入;当按序完成所有数位的排序时,原始排序序列就完成了排序;

例如:以10为基数,分别对个十百千万位作为特征,分成多个组(组数为最高位的位数,如十进制1024,则分为个十百千4个组),对于每个组,分别将排序元素"分配"到长度为10的收集数组中,最后再遍历收集数组,输出当前数位下的有序序列;
基数排序可以分为:最高位优先(MSD)和最低位优先(LSD);

package sort.radix;

import java.util.Arrays;
import java.util.List;

/**
 * 基数排序:基数排序是一种特殊的排序方式,不是基于比较进行排序,而是基于关键字的各个位置大小进行"分配"和"收集"两种操作对关键字序列进行排序的一种排序方式;
 * 基数排序本质还是属于桶排序的一种,思路就是将排序序列元素按照元素特征,拆成k个组,分别对每个组计数排序,然后再输出当前组的有序序列;
 * 下一组的排序序列则以上一组输出的有序序列为输入,当按序完成所有组的排序时,原始排序序列就完成了排序;
 * 例如:以10为基数,分别对个十百千万位作为特征,分成多个组,分别对每个组做计数排序;
 * 基数排序可以分为:最高位优先(MSD)和最低位优先(LSD);
 * refer:https://blog.csdn.net/qq_51950769/article/details/122734206
 */
public class RadixSort {

    public static int[] initData() {
        List<Integer> input = Arrays.asList(18, 3, 39, 7, 27, 211, 69, 59, 985);
        int[] array = new int[input.size()];
        for (int i = 0; i < input.size(); i++) {
            array[i] = input.get(i);
        }
        System.out.println("input:" + Arrays.toString(array));
        return array;
    }

    // 10为基数
    public static void sort(int[] src) {
        // 基数为10
        int base = 10;
        // 获取序列元素中最长的位数
        int numberLength = getMaxNumberLength(base, src);

        // 递归方式 按照位数先入桶,再输出该位下的序列,再入桶,以此类推
        buildBucketsAndOutPutArray(src, numberLength, base);
    }

    // 核心方法
    private static void buildBucketsAndOutPutArray(int[] src, int numberLength, int base) {
        // 从个位开始 各位记为0 十位记为1 以此类推
        for (int j = 0; j <= numberLength; j++) {
            // 临时数组 当然也可以使用数组链表,数组为bucket的位数桶位,List存桶位内的元素序列
            int[][] buckets = new int[base][src.length];
            // 存放当前数位的数值bucket下 元素个数 初始化都是0
            int[] itemNumInBucket = new int[base];
            // 1. 先构建buckets
            for (int i = 0; i < src.length; i++) {
                // 计算当前位数下 位数上的数值 公式:先对上一位取余数,则当前位变成了最高位,然后再对当前位取商得到当前数位的值
                // 如123的十位的值为2: 123 % 10^(2)=23, 23 / 10^(2-1) = 2
                int radix = src[i] % ((int) Math.pow(base, j + 1)) / ((int) Math.pow(base, j));
                // 存入数组
                buckets[radix][itemNumInBucket[radix]] = src[i];
                // 数量+1
                itemNumInBucket[radix]++;
            }
            // 2. 再遍历buckets 输出存放的元素到src
            int srcIndex = 0;
            for (int i = 0; i < base; i++) {
                int itemIndex = 0;
                while (itemIndex < itemNumInBucket[i]) {
                    // 对于一个bucket存放的序列 因为小的值放前面 所以输出时也要注意顺序 从前往后(类似队列先入先出)正序输出存放的元素到src
                    src[srcIndex] = buckets[i][itemIndex];
                    itemIndex++;
                    srcIndex++;
                }
            }
        }
    }

    // 先找到最大值 再算位数
    private static int getMaxNumberLength(int base, int[] src) {
        int max = 0;
        for (int item : src) {
            max = Math.max(max, item);
        }
        // 个位记为0
        int numberLength = 0;
        while (max / base > 0) {
            max = max / base;
            numberLength++;
        }
        return numberLength;
    }

    public static void main(String[] args) {
        int[] array = initData();
        sort(array);
        System.out.println("sorted:" + Arrays.toString(array));
    }
}

动图

小结

名词解释

n:数据规模

k:“桶”的个数

In-place:占用常数内存,不占用额外内存

Out-place:占用额外内存

稳定性:排序后 2 个相等键值的顺序和排序之前它们的顺序相同

关于时间复杂度

  • 平方阶 (O(n2)) 排序 各类简单排序:直接插入、直接选择和冒泡排序;
  • 线性对数阶 (O(nlog2n)) 排序 快速排序、堆排序和归并排序;
  • O(n1+§)) 排序,§ 是介于 0 和 1 之间的常数;希尔排序;
  • 线性阶 (O(n)) 排序 基数排序,此外还有桶、箱排序;

关于稳定性

  • 稳定的排序算法:冒泡排序、插入排序、归并排序和基数排序。
  • 不是稳定的排序算法:选择排序、快速排序、希尔排序、堆排序;

参考:

常用十大排序算法

一文详解十大经典排序算法

猜你喜欢

转载自blog.csdn.net/minghao0508/article/details/130017485