title: Day33-数据结构与算法-排序
date: 2020-11-30 18:21:36
author: 子陌
- 常用的经典数据结构(例如:二叉树、哈希表、Trie等)
- 更高级的数据结构(例如:图、并查集、跳表、布隆过滤器等)与各种算法(例如:排序、KMP、贪心、分治、动态规划等)
- 刷题LeetCode和算法真题(海量数据处理、字符串处理)
常用的经典数据结构
10大排序算法
- 以上表格是基于数组进行排序的一般性结论
- 冒泡、选择、插入、归并、快速、希尔、堆排序,属于比较排序(Comparison Sorting)
排序算法的稳定性
- 如果相等的两个元素,在排序前后的相对位置保持不变,那么这是稳定的排序算法
- 排序前:5,1,3a,4,7,3b
- 稳定排序:1,3a,3b,4,5,7
- 不稳定排序:1,3b,3a,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中曾经找到的最(大/小)元素,重复执行步骤1,直到全部有序
- 从头开始比较每一对相邻元素,如果第一个比第二个(大/小),就交换他们的位置
优化:(标记交换位置)
- 如果序列已经完全有序,可以提前终止冒泡排序
- 如果序列尾部已经局部有序,可以记录最后一次交换的位置,减少比较次数
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中曾经找到的最(小/大)元素,重复执行步骤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)
- 堆排序可以认为是对选择排序的一种优化
- 执行流程
- 对序列进行原地建堆(heapify)
- 重复执行以下操作,直到堆的元素数量为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)
-
确定一个元素在数组中的位置:
- 如果是无序数组,从0开始遍历搜索,平均时间复杂度:O(n)
- 如果是有序数组,可以使用二分搜索,最坏时间复杂度:O(logn)
-
假设在[begin,end)范围内搜索某个元素v,mid == (begin + end)/ 2
- 如果 v < m,去[begin,mid)范围内二分搜索
- 如果 v > m,去[mid + 1,end)范围内二分搜索
- 如果 v == m,直接返回mid
- 当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)
类似扑克牌的排序
- 执行流程
- 在执行过程中,插入排序会将序列分为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个位置
- 将待插入元素放到最终核实的位置
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)首次提出
-
执行流程
-
不断地将当前序列平均分割成2个子序列
直到不能再分割(序列中只剩1个元素)
-
不断地将2个子序列合并成一个有序序列
直到最终只剩下1个有序序列
-
归并排序 - merge细节
- 需要merge的2组序列在于同一个数组中,并且是挨在一起的
- 为了更好地完成merge操作,最好将其中1组序列备份出来,比如[begin,mid)
- 左边先结束,不用做任何操作
- 右边先结束,把左边剩下的挪到数组中去
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)
- 执行流程
- 从序列中选择一个轴点元素(pivot)
- 假设每次选择0位置的元素为轴点元素
- 利用pivot将序列分割成2个子序
- 将小于pivot的元素放在pivot前面(左侧)
- 将大于pivot的元素防止pivot后面(右侧)
- 等于pivot的元素放哪边都可以
- 对子序列进行1,2操作
- 直到不能再分割(子序列中只剩下1个元素)
- 从序列中选择一个轴点元素(pivot)
- 快速排序的本质
- 逐渐将每个元素都转换成轴点元素
快速排序 - 执行流程
快速排序 - 轴点相等的元素
- 如果序列中的所有元素都与轴点元素相等,利用目前的算法实现,轴点元素可以将序列分割成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
- K(整数)从0开始
计数排序(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是进制
基数排序另一种思路
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)
- 执行流程
- 创建一定数量的桶(比如用数组、链表作为桶)
- 按照一定的规则(不同类型的数据,规则不同),将序列中的元素均匀分配到对应的桶
- 分别对每个桶进行单独排序
- 将所有非空桶的元素合并成有序序列
- 元素在桶中的索引
- 元素值 * 元素数量
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