Day33-数据结构与算法-排序


title: Day33-数据结构与算法-排序
date: 2020-11-30 18:21:36
author: 子陌


  1. 常用的经典数据结构(例如:二叉树、哈希表、Trie等)
  2. 更高级的数据结构(例如:图、并查集、跳表、布隆过滤器等)与各种算法(例如:排序、KMP、贪心、分治、动态规划等)
  3. 刷题LeetCode和算法真题(海量数据处理、字符串处理)

第二季

常用的经典数据结构

10大排序算法

十大排序算法

  • 以上表格是基于数组进行排序的一般性结论
  • 冒泡、选择、插入、归并、快速、希尔、堆排序,属于比较排序(Comparison Sorting)

排序算法的稳定性

  • 如果相等的两个元素,在排序前后的相对位置保持不变,那么这是稳定的排序算法
    • 排序前:5,1,3a,4,7,3b
    • 稳定排序:1,3a3b,4,5,7
    • 不稳定排序:1,3b3a,4,5,7
  • 对自定义对象进行排序时,稳定性会影响最终的排序效果

原地算法(In-place Algorithm)

  • 不依赖额外的资源或者依赖少数的额外资源,仅仅依靠输出来覆盖输入
  • 空间复杂度为O(1)的都可以认为是原地算法

非原地算法(Not-In-place or Out-of-place)

排序公共基类抽取

package com.zimo.算法.排序.基类;

import java.text.DecimalFormat;

/**
 * 抽象排序基类
 *
 * @author Liu_zimo
 * @version v0.1 by 2020/12/1 16:49
 */
public abstract class Sort<E extends Comparable<E>> implements Comparable<Sort<E>> {
    protected E[] array;  // 排序数组
    private int cmpCount;       // 比较次数
    private int swapCount;      // 交换次数
    private long time;          // 耗时

    private DecimalFormat fmt = new DecimalFormat("#.00");
	// 提供外部使用排序
    public void sort(E[] array){
        if (array == null || array.length < 2){
            return;
        }
        this.array = array;
        long start = System.currentTimeMillis();
        sort();
        time = System.currentTimeMillis() - start;
    }
	// 子类排序算法实现
    protected abstract void sort();

    protected int cmp(int i1, int i2){
        cmpCount++;
        return array[i1].compareTo(array[i2]);
    }
    protected int cmp(E v1, E v2){
        cmpCount++;
        return v1.compareTo(v2);
    }

    protected void swap(int i1, int i2){
        swapCount++;
        E tmp = array[i1];
        array[i1] = array[i2];
        array[i2] = tmp;
    }

    private String numberString(int number){
        if (number < 10000) return "" + number;
        if (number < 100000000) return fmt.format(number / 10000.0) + "万";
        return fmt.format(number / 100000000.0) + "亿";
    }

    @Override
    public String toString() {
        String timeStr ="耗时: " + (time / 1000.0) + "s(" + time + "ms)";
        String compareCountStr = "比较: " + numberString(cmpCount);
        String swapCountStr = "交换: " + numberString(swapCount);
        return "【" + getClass().getSimpleName() + "】\n"
                + timeStr + "\t"
                +compareCountStr +"\t "+swapCountStr + "\n"
                +"----------------------------------------------------";
    }
}

冒泡排序(Bubble Sort)

冒泡排序也叫做起泡排序,冒泡排序属于稳定的排序算法,稍有不慎,稳定的算法可能写成不稳定的排序算法(如果a <= b交换就是不稳定的),冒泡排序属于in-place

  • 执行流程
    1. 从头开始比较每一对相邻元素,如果第一个比第二个(大/小),就交换他们的位置
      • 执行一轮后,最末尾的那个元素就是最(大/小)元素
    2. 忽略1中曾经找到的最(大/小)元素,重复执行步骤1,直到全部有序

优化:(标记交换位置)

  1. 如果序列已经完全有序,可以提前终止冒泡排序
  2. 如果序列尾部已经局部有序,可以记录最后一次交换的位置,减少比较次数
package com.zimo.算法.排序.冒泡排序;

/**
 * 冒泡排序
 *
 * @author Liu_zimo
 * @version v0.1 by 2020/12/1 10:47
 */
public class BubbleSort {
    public static int[] sort(int[] arr){
        if (arr == null || arr.length < 2){
            return arr;
        }
        int length = arr.length;
        for (int i = length - 1; i > 0; i--) {
            int sortedIndex = 1;  // 初始值为完全有序时候用,就不会进入if,索引不会覆盖,一轮扫描直接结束
            for (int j = 1; j <= i; j++) {
                if (arr[j] < arr[j - 1]){
                    arr[j] = arr[j] ^ arr[j - 1];
                    arr[j - 1] = arr[j] ^ arr[j - 1];
                    arr[j] = arr[j] ^ arr[j - 1];
                    sortedIndex = j; // 记录最后一次交换的位置
                }
            }
            i = sortedIndex;
        }
        return arr;
    }
}

选择排序(Selection Sort)

  • 执行流程
    1. 从序列中找出最(小/大)的那个元素,然后与最(头/尾)的元素交换位置
      • 执行一轮后,最(头/尾)的那个元素就是最(小/大)的元素
    2. 忽略1中曾经找到的最(小/大)元素,重复执行步骤1
  • 选择排序的交换次数要远远少于冒泡排序,平均性能优于冒泡排序
  • 最好、最坏、平均时间复杂度:O(n²),空间复杂度:O(1),属于不稳定排序(链表排序除外)

优化:(堆)

  • 使用堆来选择最大值
package com.zimo.算法.排序.选择排序;

/**
 * 选择排序
 *
 * @author Liu_zimo
 * @version v0.1 by 2020/12/1 14:32
 */
public class SelectionSort {
    public static int[] sort(int[] arr){
        if (arr == null || arr.length < 2){
            return arr;
        }
        int length = arr.length;
        for (int i = length - 1; i > 0; i--) {
            int maxIndex = 0;
            for (int j = 1; j <= i; j++) {
                if (arr[maxIndex] <= arr[j]){   // 为了使算法稳定,这里使用<=
                    maxIndex = j; // 记录最大的位置索引
                }
            }
            int tmp = arr[i];
            arr[i] = arr[maxIndex];
            arr[maxIndex] = tmp;
        }
        return arr;
    }
}

堆排序(Heap Sort)

  • 堆排序可以认为是对选择排序的一种优化
  • 执行流程
    1. 对序列进行原地建堆(heapify)
    2. 重复执行以下操作,直到堆的元素数量为1
      • 交换堆顶元素与尾元素
      • 对的元素数量减1
      • 对0位置进行1次siftDown操作
  • 最好、最坏、平均时间复杂度:O(nlogn),空间复杂度:O(1),属于不稳定排序

堆排序

package com.zimo.算法.排序.堆排序;

/**
 * 堆排序
 *
 * @author Liu_zimo
 * @version v0.1 by 2020/12/1 15:16
 */
public class HeapSort {
    private static int heapSize;   // 堆的剩余数量
    public static int[] sort(int[] arr){
        if (arr == null || arr.length < 2){
            return arr;
        }
        heapSize = arr.length;
        // 原地建堆
        for (int i = (heapSize >> 1) - 1; i >= 0; i--){
            siftDown(i, arr);
        }
        while (heapSize > 1){
            int tmp = arr[0];   // 交换堆顶和尾部元素
            arr[0] = arr[heapSize - 1];
            arr[heapSize - 1] = tmp;
            heapSize--; 
            siftDown(0, arr);
        }
        return arr;
    }
	// 自下而上的下滤
    private static void siftDown(int index, int[] elements) {
        Integer element = elements[index];
        int half = heapSize >> 1;
        while(index < half) {
            int childIndex = (index << 1) + 1;
            Integer childElement = elements[childIndex];
            int rightIndex = childIndex + 1;
            if (rightIndex < heapSize && elements[rightIndex] > childElement){
                childElement = elements[childIndex = rightIndex];
            }
            if (element >= childElement)break;
            elements[index] = childElement;
            index = childIndex;
        }
        elements[index] = element;
    }
}

二分搜索(Binary Search)

  • 确定一个元素在数组中的位置:

    1. 如果是无序数组,从0开始遍历搜索,平均时间复杂度:O(n)
    2. 如果是有序数组,可以使用二分搜索,最坏时间复杂度:O(logn)
  • 假设在[begin,end)范围内搜索某个元素v,mid == (begin + end)/ 2

    1. 如果 v < m,去[begin,mid)范围内二分搜索
    2. 如果 v > m,去[mid + 1,end)范围内二分搜索
    3. 如果 v == m,直接返回mid
    4. 当begin == end时,查找失败
  • 区间设计成左闭右开时,end - begin就可以得到区间元素个数

  • 如果存在多个重复值,返回值不确定

package com.zimo.算法;

/**
 * 二分查找
 *
 * @author Liu_zimo
 * @version v0.1 by 2020/12/2 16:30
 */
public class BinarySearch {
    public static int indexOf(int[] array, int v){
        if (array == null || array.length == 0) return -1;
        // 设计成左闭右开时, end - begin就是区间元素的数量
        int begin = 0;
        int end = array.length;
        while (begin < end){
            int mid = (begin + end) / 2;
            if (v < array[mid]){
                end = mid;
            }else if (v > array[mid]){
                begin = mid + 1;
            }else {
                return mid;
            }
        }
        return -1;
    }
}

插入排序(Insertion Sort)

类似扑克牌的排序

  • 执行流程
    1. 在执行过程中,插入排序会将序列分为2部分
      • 头部是已经排好序的,尾部是待排序的
    2. 从头开始扫描每一个元素
      • 每当扫描到一个元素,就将它插入到头部合适的位置,使得头部数据依然保持有序
package com.zimo.算法.排序.插入排序;

/**
 * 插入排序
 *
 * @author Liu_zimo
 * @version v0.1 by 2020/12/2 14:03
 */
public class InsertionSort {
    public static int[] sort(int[] arr){
        if (arr == null || arr.length < 2){
            return arr;
        }
        for (int i = 1; i < arr.length; i++) {
            int current = i;
            for (int j = current - 1; j >= 0; j--) {
                if (arr[current] < arr[j]){
                    int tmp = arr[current];
                    arr[current] = arr[j];
                    arr[j] = tmp;
                    current--;
                }
            }
        }

        return arr;
    }
}

插入排序 - 逆序对(Inversion)

  • 数组<2,3,8,6,1>的逆序对为:<2,1> <3,1> <8,1> <6,1>,共5个逆序对
  • 插入排序的时间复杂度与逆序对的数量成正比关系
    • 逆序对的数量越多,插入排序的时间复杂度就越高
  • 最坏、平均时间复杂度:O(n²)
  • 最好时间复杂度:O(n)
  • 空间复杂度:O(1)
  • 属于稳定排序
  • 当逆序对的数量极少时,插入排序的效率特别高
    • 甚至速度比O(nlogn)级别的快速排序还要快
  • 数据量不是特别大的时候,插入排序的效率也是非常好的

优化一:(挪动优化)

  • 将交换转为挪动
    1. 先将待插入的元素备份
    2. 头部有序数据中比待插入元素大的,都朝尾部方向挪动1个位置
    3. 将待插入元素放到最终核实的位置
for (int i = 1; i < arr.length; i++) {
    int current = i;
    int element = arr[current];     	// 把要插入的元素缓存下来
    for (int j = current - 1; j >= 0; j--) {
        if (element < arr[j]){			// element一直往前比较
            arr[current--] = arr[j];	// 如果大先往后挪
        }
    }
    arr[current] = element;				// 全部比较晚再覆盖
}

优化二:(二分搜索优化)

  • 在元素v的插入过程中,可以先二分搜索出最合适的插入位置,然后再将元素v插入
  • 要求二分搜索返回的插入位置:第一个大于v的元素位置
package com.zimo.算法.排序.插入排序;

import com.zimo.算法.排序.基类.Sort;

/**
 * 插入排序 - 重构(二分查找优化版)
 *
 * @author Liu_zimo
 * @version v0.1 by 2020/12/2 17:13
 */
public class InsertionSortRe1<E extends Comparable<E>> extends Sort<E> {
    @Override
    protected void sort() {
        for (int i = 1; i < array.length; i++) {
            E element = array[i];
            int insertIndex = search(i);
            for (int j = i; j > insertIndex; j--){
                array[j] = array[j-1];
            }
        }
    }

    private int search(int index){
        int begin = 0;
        int end = index;
        while (begin < end){
            int mid = (begin + end) >> 1;
            if (cmp(array[index], array[mid]) < 0){
                end = mid;
            }else{
                begin = mid + 1;
            }
        }
        return begin;
    }
    @Override
    public int compareTo(Sort<E> eSort) {
        return 0;
    }
}
  • 使用了二分搜索后,只是减少了比较次数,但是插入排序的平均时间复杂度依然是O(n²)

归并排序(Merge Sort)

  • 1945年由约翰·冯诺依曼(John von Neumann)首次提出

  • 执行流程

    1. 不断地将当前序列平均分割成2个子序列

      直到不能再分割(序列中只剩1个元素)

    2. 不断地将2个子序列合并成一个有序序列

      直到最终只剩下1个有序序列

归并排序

归并排序 - merge细节

  • 需要merge的2组序列在于同一个数组中,并且是挨在一起的
  • 为了更好地完成merge操作,最好将其中1组序列备份出来,比如[begin,mid)
  • 左边先结束,不用做任何操作
  • 右边先结束,把左边剩下的挪到数组中去

归并排序-merge

package com.zimo.算法.排序.归并排序;

import com.zimo.算法.排序.基类.Sort;

/**
 * 归并排序
 *
 * @author Liu_zimo
 * @version v0.1 by 2020/12/2 18:26
 */
public class MergeSort<E extends Comparable<E>> extends Sort<E> {
    private E[] leftArray;  // 直接开辟最大的左边数组,循环利用,避免merge重复开销
    @Override
    protected void sort() {
//        leftArray = (E[]) new Object[array.length >> 1];
        leftArray = (E[]) new Comparable[array.length >> 1];    // 数组必须具备可比较性
        divide(0, array.length);
    }

    /**
     * 对[begin,end)范围的数据进行归并排序(拆分)
     * @param begin
     * @param end
     */
    private void divide(int begin, int end) {
        if (end - begin < 2) return;
        int mid = (begin + end) >> 1;   // 对数组进行拆分
        divide(begin, mid);             // [begin, leng/2)
        divide(mid, end);               // [leng/2, end)
        merge(begin, mid, end);
    }

    /**
     * 将 [begin, mid)和[mid, end)范围的序列合并成一个有序序列(合并)
     * @param begin
     * @param mid
     * @param end
     */
    private void merge(int begin, int mid, int end) {
        int leftIndex = 0, leftEnd = mid - begin;   // 左边数组(基于leftArray)
        int rightIndex = mid, rightEnd = end;       // 右边数组
        int currentIndex = begin;                   // array索引

        // 备份左边的数组
        for (int i = leftIndex; i < leftEnd; i++) {
            this.leftArray[i] = this.array[begin + i];
        }

        // 如果左边没结束,就继续操作
        while (leftIndex < leftEnd){
            if (rightIndex < rightEnd && cmp(array[rightIndex], leftArray[leftIndex]) < 0){
                array[currentIndex++] = array[rightIndex++];      // 拷贝右边数组到array
            }else {
                array[currentIndex++] = leftArray[leftIndex++]; // 拷贝左边数组到array
            }   // 如果cmp位置改为 <= 0 会失去稳定性
        }
    }

    @Override
    public int compareTo(Sort<E> eSort) {
        return 0;
    }
}
  • 归并排序花费的时间
    • T(n) = 2 * T(n/2) + O(n)
    • T(1) = O(1)
    • T(n)/n = T(n/2)/(n/2) + O(1)
  • 令S(n) = T(n) / n
    • S(1) = O(1)
    • S(n) = S(n/2) + O(1) = S(n/4) + O(2) = S(n/8) + O(3) = S(n/2k) + O(k) = S(1) + O(logn) = O(logn)
  • 由于归并排序总是平均分割子序列,所以最好、最坏、平均时间复杂度都是O(nlogn),属于稳定排序
  • 从代码上不难看出:归并排序的空间复杂度是O(n/2 + logn) = O(n)
递推式复杂度
T(n) = T(n / 2) + O(1)O(logn)
T(n) = T(n - 1) + O(1)O(n)
T(n) = T(n / 2) + O(n)O(n)
T(n) = 2 * T(n / 2) + O(1)O(n)
T(n) = 2 * T(n / 2) + O(n)O(nlogn)
T(n) = T(n - 1) + O(n)O(n²)
T(n) = 2 * T(n - 1) + O(1)O(2n)
T(n) = 2 * T(n - 1) + O(n)O(2n)

快速排序(Quick Sort)

1960年由查尔斯·安东尼·理查德·霍尔(Charles Antony Richard Hoare),昵称为东尼·霍尔(Tony Hoare)

  • 执行流程
    1. 从序列中选择一个轴点元素(pivot)
      • 假设每次选择0位置的元素为轴点元素
    2. 利用pivot将序列分割成2个子序
      • 将小于pivot的元素放在pivot前面(左侧)
      • 将大于pivot的元素防止pivot后面(右侧)
      • 等于pivot的元素放哪边都可以
    3. 对子序列进行1,2操作
      • 直到不能再分割(子序列中只剩下1个元素)
  • 快速排序的本质
    • 逐渐将每个元素都转换成轴点元素

快速排序

快速排序 - 执行流程

快速排序-轴点构造

快速排序 - 轴点相等的元素

轴点相等的元素

  • 如果序列中的所有元素都与轴点元素相等,利用目前的算法实现,轴点元素可以将序列分割成2个均匀的子序列
package com.zimo.算法.排序.快速排序;

import com.zimo.算法.排序.基类.Sort;

/**
 * 快速排序
 *
 * @author Liu_zimo
 * @version v0.1 by 2020/12/9 15:22:59
 */
public class QuickSort<E extends Comparable<E>> extends Sort<E> {

    @Override
    protected void sort() {
        sort(0, array.length);
    }

    /**
     * 对[begin, end)范围的元素进行快速排序
     * @param begin
     * @param end
     */
    private void sort(int begin, int end){
        if (end - begin < 2) return;
        // 确定轴点的位置
        int pivotIndex = pivotIndex(begin, end);
        // 对子序列进行排序
        sort(begin, pivotIndex);
        sort(pivotIndex + 1, end);
    }

    /**
     * 构造出[begin, end)范围的轴点元素
     * @return 返回轴点元素的最终位置
     */
    private int pivotIndex(int begin, int end){
        // 优化一:将轴点随机化,原先固定begin位置就是轴点
        // [0,1) * (end - begin) + begin = [0 + begin, (end-begin) + begin)
        int random = (int) Math.random() * (end - begin) + begin;
        swap(begin, random);

        // 备份begin位置的元素
        E pivot = array[begin];
        end--;

        while (begin < end){
            while (begin < end) {
                // 从右往左
                if (cmp(pivot, array[end]) < 0) {    // 右边元素 > 轴点元素
                    end--;
                } else {     // 右边元素 <= 轴点元素
                    this.array[begin++] = array[end];
                    break;  // 交替执行
                }	// 如果条件变成 <= 切割出来的轴点会导致不均匀
            }
            while (begin < end) {
                // 从左往右
                if (cmp(pivot, array[begin]) > 0) {  // 左边元素 < 轴点元素
                    begin++;
                } else {     // 左边元素 >= 轴点元素
                    this.array[end--] = array[begin];
                    break;  // 交替执行
                }	// 如果条件变成 >= 切割出来的轴点会导致不均匀
            }
        }
        // 将轴点元素翻入最终位置
        array[begin] = pivot;
        // 返回轴点元素的位置begin或者end
        return begin;
    }

    @Override
    public int compareTo(Sort<E> eSort) {
        return 0;
    }
}

优化:(随机选择轴点,降低左右两边数量不均匀)

  • 轴点元素分割出来的子序列嫉妒不均匀
    • 导致出现最坏的时间复杂度O(n²)

时间复杂度

  • 在轴点左右元素数量比较均匀的情况下,同时也是最好的情况
    • T(n) = 2 * T(n/2) + O(n) = O(nlogn)
  • 如果轴点左右元素数量极度不均匀,最坏情况
    • T(n) = T(n - 1) + O(n) = O(n²)
  • 为了降低最坏情况的出现概率,一般采取的做法是
    • 随机选择轴点元素
  • 最好、平均时间复杂度:O(nlogn)
  • 最坏时间复杂度:O(n²)
  • 由于递归调用的缘故,空间复杂度:O(logn)

希尔排序(Shell Sort)

  • 1959年由唐纳德·希尔(Donald Shell)提出
  • 希尔排序把序列看作是一个矩阵,分成m列,逐列进行排序
    • m从某个整数逐渐减为1
    • 当m为1时,整个序列将完全有序
  • 因此,希尔排序也被称为递减增量排序(Diminishing Increment Sort)
  • 矩阵的列数取决于步长序列(step sequence)
    • 比如,如果步长序列为{1,5,19,41,109,…},就代表一次分成109列、41列、19列、5列、1列进行排序
    • 不同的步长序列,执行效率也不同

希尔排序 - 实例

  • 希尔本人给出的步长序列是(n / 2k),比如n为16时,步长序列是{1,2,4,8}

希尔排序

  • 不难看出,从8列变为1列的过程中,逆序对的数量在逐渐减少

    • 因此希尔排序底层一般使用插入排序对每一列进行排序,也很多资料认为希尔排序是插入排序的改进版
  • 假设元素在第col列、第row行,步长(总列数)是step

    • 那么这个元素在数组中的索引是col + row * step

希尔排序 - 实现

  • 最好情况是步长序列只有1,且序列几乎有序,事件复杂度为O(n)
  • 空间复杂度为O(1)
  • 不属于稳定排序
  • 希尔本人给出的步长序列,最坏情况事件复杂度是O(n²)
package com.zimo.算法.排序.希尔排序;

import com.zimo.算法.排序.基类.Sort;

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

/**
 * 希尔排序
 *
 * @author Liu_zimo
 * @version v0.1 by 2020/12/11 10:22:58
 */
public class ShellSort<E extends Comparable<E>> extends Sort<E> {

    @Override
    protected void sort() {
        List<Integer> stepSequence = shellStepSequence();
        for (Integer step: stepSequence) {
            sort(step);
        }
    }

    /**
     * 分成step列进行排序
     * @param step
     */
    private void sort(int step){
        for (int col = 0; col < step; col++) {  // 对第col列进行排序
            for (int begin = col + step; begin < array.length; begin+=step) {  // 插入排序算法
                int cur = begin;
                while (cur > col && cmp(cur, cur - step) < 0){
                    swap(cur, cur - step);
                    cur -= step;
                }
            }
        }
    }
    // 希尔给出的步长序列 n / 2^k , 最坏情况O(n²)
    private List<Integer> shellStepSequence(){
        List<Integer> list = new ArrayList<>();
        int step = array.length;
        while ((step >>= 1) > 0){
            list.add(step);
        }
        return list;
    }
    // 目前最好的步长
    private List<Integer> bestStepSequence(int count){  // count:数据规模 array.length,最坏情况O(n的1.33..次方)
        List<Integer> list = new ArrayList<>();
        int k = 0, step = 0;
        while (true){
            if (k % 2 == 0){
                int pow = (int) Math.pow(2, k >> 1);
                step = 1 + 9 * (pow * pow - pow);
            }else {
                int pow1 = (int) Math.pow(2, (k - 1) >> 1);
                int pow2 = (int) Math.pow(2, (k + 1) >> 1);
                step = 1 + 8 * pow1 * pow2 - 6 * pow2;
            }
            if (step >= count) break;
            list.add(0, step);
            k++;
        }
        return list;
    }

    @Override
    public int compareTo(Sort<E> eSort) {
        return 0;
    }
}
  • 目前已知最好的步长序列,最坏情况事件复杂度是O(n4/3),1986年由Robert Sedgewick提出
    • K(整数)从0开始
      • K为偶数:9(2k - 2k/2) + 1
      • K为奇数:8 + 2k - 6 · 2(k + 1) / 2 + 1
    • 1,5,19,41,109

计数排序(Counting Sort)

  • 之前学习的冒泡、选择、插入、归并、快速、希尔、堆排序,都是基于比较的排序
    • 平均时间复杂度目前最低是O(nlogn)
  • 计数排序、桶排序、基数排序,都不是基于比较的排序
    • 它们是典型的用空间换时间,在某些时候,平均时间复杂度可以比O(nlogn)更低
  • 计数排序于1954年由Harold H.Seward提出,适合对一定范围内的整数进行排序
  • 计数排序的核心思想
    • 统计每个整数在序列中出现的次数,进而推导出每个整数在有序序列中的索引

计数排序最简单的实现

计数排序简单实现

protected void sort() {
    // 找出最大值
    int max = array[0];
    for (int i = 1; i < array.length; i++) {
        if (array[i] > max) max = array[i];
    }
    // 开辟内存空间,存储每个整数出现次数
    int[] counts = new int[1 + max];
    // 统计每个整数出现的次数
    for (int i = 0; i < array.length; i++) {
        counts[array[i]]++;
    }
    // 根据整数的出现次数,对整数进行排序
    int index = 0;
    for (int i = 0; i < counts.length; i++) {
        if (counts[i] > 0) {
            while (counts[i]-- > 0) {
                array[index++] = i;
            }
        }
    }
}
  • 该版本的实现存在以下几个问题
    • 无法对负整数进行排序
    • 极其浪费内存空间
    • 是个不稳定的排序
    • 只能对整数进行排序

计数排序改进版

  • 优化:
    • 内存空间:min - max
    • 对负整数进行排序

计数排序优化

package com.zimo.算法.排序.计数排序;

import com.zimo.算法.排序.基类.Sort;

/**
 * 计数排序 - 改进版
 *      1. 优化内存空间
 *      2. 可以对负数进行排序
 *      3. 是个稳定的排序
 *
 * @author Liu_zimo
 * @version v0.1 by 2020/12/11 15:33
 */
public class CountingSortRe extends Sort<Integer> {

    @Override
    protected void sort() {
        // 找出最大值和最小值
        int max = array[0];
        int min = array[0];
        for (int i = 1; i < array.length; i++) {
            if (array[i] > max) max = array[i];
            if (array[i] < min) min = array[i];
        }
        // 开辟内存空间,存储次数
        int[] counts = new int[max - min + 1];
        // 统计每个数出现的次数
        for (int i = 0; i < array.length; i++) {
            counts[array[i] - min]++;
        }
        // 累加次数
        for (int i = 1; i < counts.length; i++) {
            counts[i] += counts[i-1];
        }
        // 从后往前遍历元素,将它放到有序数组中的合适位置(从后往前修复排序稳定)
        int[] newArray = new int[array.length];
        for (int i = array.length - 1; i >= 0; i--) {
            int count = --counts[array[i] - min];
            newArray[count] = array[i];
        }
        // 将有序数组复制到array中
        for (int i = 0; i < newArray.length; i++) {
            array[i] = newArray[i];
        }
    }

    @Override
    public int compareTo(Sort<Integer> integerSort) {
        return 0;
    }
}
  • 最好、最坏、平均时间复杂度:O(n + k)
  • 空间复杂度:O(n + k)
  • k是整数的取值范围

计数排序对自定义对象进行排序

  • 如果自定义对象可以提供用以排序的整数类型,依然可以使用计数排序(例如:Person.age按人的年龄进行排序)

基数排序(Radix Sort)

  • 基数排序非常适合用于整数排序(尤其是非负整数),因此只演示对非负整数进行基数排序
  • 执行流程:依次对个位数、十位数、百位数、千位数、万位数…进行排序(从低位到高位)
  • 个位数、十位数、百位数的取值范围都是固定的0~9,可以使用计数排序对它们进行排序

基数排序

package com.zimo.算法.排序.基数排序;

import com.zimo.算法.排序.基类.Sort;

/**
 * 基数排序
 *
 * @author Liu_zimo
 * @version v0.1 by 2020/12/14 14:48
 */
public class RadixSort extends Sort<Integer> {
    @Override
    protected void sort() {
        // 找出最大值
        int max = array[0];
        for (int i = 1; i < array.length; i++) {
            if (array[i] > max) max = array[i];
        }
        for (int divider = 1; divider <= max; divider *= 10) {
            countingSort(divider);
        }
    }

    private void countingSort(int devider){
        // 开辟内存空间,存储次数 0 - 9 min = 0
        int[] counts = new int[10];
        // 统计每个数出现的次数
        for (int i = 0; i < array.length; i++) {
            counts[array[i] / devider % 10]++;
        }
        // 累加次数
        for (int i = 1; i < counts.length; i++) {
            counts[i] += counts[i-1];
        }
        // 从后往前遍历元素,将它放到有序数组中的合适位置(从后往前修复排序稳定)
        int[] newArray = new int[array.length];
        for (int i = array.length - 1; i >= 0; i--) {
            int count = --counts[array[i] / devider % 10];
            newArray[count] = array[i];
        }
        // 将有序数组复制到array中
        for (int i = 0; i < newArray.length; i++) {
            array[i] = newArray[i];
        }
    }

    @Override
    public int compareTo(Sort<Integer> integerSort) {
        return 0;
    }
}
  • 最好、最坏、平均时间复杂度:O(d * (n + k)),d是最大值的位数,k是进制。属于稳定排序
  • 空间复杂度:O(n + k),k是进制

基数排序另一种思路

基数排序1

protected void sort() {
    // 找出最大值
    int max = array[0];
    for (int i = 1; i < array.length; i++) {
        if (array[i] > max) max = array[i];
    }
    //桶数组
    int[][] buckets = new int[10][array.length];
    //每个桶的元素数量
    int[] bucketSizes = new int[buckets.length];
    for (int divider = 1; divider <= max; divider *= 10){
        for (int i = 0; i < array.length; i++){
            int no = array[i] / divider % 10;
            buckets[no][bucketSizes[no]++] = array[i];      // 将数组元素放到对应的桶里面
        }
        int index = 0;
        for (int i = 0; i < buckets.length; i++){
            for (int j =0; j < bucketSizes[i];j++){
                array[index++] = buckets[i][j];     // 桶里面取出数据放到数组中
            }
            bucketSizes[i] = 0;
        }
    }
}
  • 空间复杂度是O(kn + k),时间复杂度是O(dn)
  • d是最大值的位数,k是进制

桶排序(Bucket Sort)

  • 执行流程
    1. 创建一定数量的桶(比如用数组、链表作为桶)
    2. 按照一定的规则(不同类型的数据,规则不同),将序列中的元素均匀分配到对应的桶
    3. 分别对每个桶进行单独排序
    4. 将所有非空桶的元素合并成有序序列
  • 元素在桶中的索引
    • 元素值 * 元素数量

桶排序

package com.zimo.算法.排序.桶排序;

import com.zimo.算法.排序.基类.Sort;

import java.util.LinkedList;
import java.util.List;

/**
 * 桶排序
 *
 * @author Liu_zimo
 * @version v0.1 by 2020/12/14 17:10
 */
public class BucketSort extends Sort<Double> {
    @Override
    protected void sort() {
        //桶数组
        List<Double>[] buckets = new List[array.length];
        for (int i = 0; i <array.length; i++){
            int bucketIndex = (int) (array[i] * array.length);
            List<Double> bucket = buckets[bucketIndex];
            if (bucket == null){
                bucket = new LinkedList<>();
                buckets[bucketIndex] = bucket;
            }
            bucket.add(array[i]);
        }
        //对每个桶进行排序
        int index = 0;
        for (int i = 0; i < buckets.length; i++){
            if (buckets[i] == null) continue;
            buckets[i].sort(null);
            for (Double d : buckets[i]){
                array[index++] = d;
            }
        }
    }

    @Override
    public int compareTo(Sort<Double> sort) {
        return 0;
    }
}
  • 空间复杂度是:O(n + m),m是桶的数量
  • 时间复杂度:O(n) + m * O((n/m) * log(n/m)) = O(n + n * log(n/m)) = O(n + n * logn - n * logm)
    • 因此为O(n + k),k为n * logn - n * logm
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

柳子陌

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值