数据结构与算法——十大排序
排序
1.排序的基本定义
排序:就是使一串记录,按照其中的某个或者某些关键字的大小,递增或递减的排列起来的操作。
排序算法:就是如何使得记录按照要求排列的方法。排序算法在很多领域都得到很大的重视,尤其在大量数据的处理方面。一个优秀的算法可以节省大量的资源。
稳定:A=B,排序前A在B前,排序后A仍在B前
不稳定:A=B,排序前A在B前,排序后B在A前
2.排序的分类
- 比较类排序:通过比较来决定元素间的相对次序,由于其时间复杂度不能突破O(nlogn),因此也称为非线性时间比较类排序
- 非比较类排序:不通过比较来决定元素间的相对次序,它可以突破基于比较排序的时间下界,以线性时间运行,因此也称为线性时间非比较类排序。
3.排序的时间复杂度和空间复杂度
4.比较执行效率
(1)各种情况下的时间复杂度
(2)时间复杂度的系数常数等
(3)比较次数或移动次数
十大排序算法
本博客基于以下优秀博客合写,并加入自己的一些手写图例
【参考阅读:十大经典排序算法;十大经典排序算法(含优化);十大经典排序算法动画与解析,看我就够了!(配代码完全版)
】
1. 冒泡排序(Bubble Sort)
1.1 定义
冒泡排序(Bubble Sort):是一种典型的交换排序算法,通过交换数据元素的位置进行排序。
关键的两步操作:比较和交换
1.2 基本思想
从无序数组的头部开始,两两比较,根据大小交换位置,直到最大(最小)的数移到该数组尾部,循环前面的操作,完成排序。
1.3 描述
(1)比较相邻的元素,如果第一个比第二个大,就交换它们的位置;
(2)对每一对相邻元素作同样的工作,从开始第一对进行到结尾的最后一对,这样在最后的元素将是最大的数;
(3)针对所有位置的元素重复以上的步骤,除了最后一个;
(4)持续每次对越来越少的元素(无序元素)重复上面的步骤,直到没有任何一对相邻的数组需要比较,则序列最终有序。
1.4 动图示例
以实际代码为例:(标志变量版)
1.5 代码实现
package ten_sort;
public class Bubble_sort {
// 比较和交换
// 普通版冒泡排序
public static int[] bubbleSort(int[] arr) {
// i控制排序次数,j控制比较次数
int i, j, temp, len = arr.length;
//最后一个元素自动有序
for (i = 0; i < len - 1; i++) {
for (j = 0; j < len - i - 1; j++) {
//此处为比较和交换
if (arr[j] < arr[j + 1]) {
temp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = temp;
}
}
}
return arr;
}
public static void main(String[] args) {
// TODO Auto-generated method stub
int[] arr = { 3, 5, 32, 2, 12, 2, 11 };
int[] bubbleArray = bubbleSort(arr);
for (int i = 0; i < bubbleArray.length; i++) {
System.out.print(bubbleArray[i] + " ! ");
}
}
}
1.6 改进一(加上标志变量)
在外层循环里面加入一个标志变量flag,如果在一次冒泡过程中发现没有发送数据交换,则认为当前数组已经有序,则改变flag变量的值,循环终止。
减少了循环次数!
实现代码如下:
package ten_sort;
public class Bubble_sort_up1 {
// 自动生成返回值对象的快捷键 ctrl+1+enter
// 放大缩小ctrl + -
public static int[] bubbleSort(int[] arr) {
int i, j, temp, len = arr.length, flag = 1, count = 0;
for (i = 0; i < len - 1 && flag != 0; i++) {
count++;
flag = 0;
for (j = 0; j < len - i - 1; j++) {
if (arr[j] < arr[j + 1]) {
temp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = temp;
flag = 1;
}
}
}
System.err.println(count);
return arr;
}
public static void main(String[] args) {
// TODO Auto-generated method stub
int[] arr = { 3, 5, 32, 2, 12, 2, 11 };
int[] bubbleArray = bubbleSort(arr);
for (int i = 0; i < bubbleArray.length; i++) {
System.out.println(bubbleArray[i] + " ! ");
}
}
}
1.7 改进二(来回排序)
在每趟排序过程中进行正向和反向两次冒泡的方法,这样一次排序可以得到两个最终值(最大值和最小值),从而使排序趟数几乎减少了一半。
实际上,如果数组在乱序的状态下,鸡尾酒排序和传统的冒泡排序效率都很差劲
package ten_sort;
public class Bubble_sort_up2 {
// 来回排序/鸡尾酒排序
public static void rebackSort(int arr[]) {
int i, temp, left = 0, right = arr.length - 1;
while (left < right) {
// 正向冒泡,左至右,找最大
for (i = left; i < right; i++) {
if (arr[i] > arr[i + 1]) {
temp = arr[i];
arr[i] = arr[i + 1];
arr[i + 1] = temp;
}
}
// 向左移动一位
right--;
// 反向冒泡,从右往左,找最小
for (i = right; i > left; i--) {
if (arr[i - 1] > arr[i]) {
temp = arr[i];
arr[i] = arr[i - 1];
arr[i - 1] = temp;
}
}
// 向右移一位
left++;
}
for (int k = 0; k < arr.length; k++) {
System.out.println(arr[k] + " ! ");
}
}
public static void main(String[] args) {
// TODO Auto-generated method stub
int[] arr = { 3, 5, 32, 2, 12, 2, 11 };
rebackSort(arr);
}
}
1.8 性能分析
简而言之:时间O(n²),空间O(1),稳定
2. 插入排序
2.1 定义
插入排序(Insertion Sort):是一种简单直观的排序算法。它的工作原理是通过构件有序序列,对于未排序的数据元素,在已排序序列中从后向前扫描,找到相应的位置并插入。
2.2 基本思想
顺序将未排序的各元素依据关键字大小插入已排序的序列中
2.3 描述
(1)从第一个元素开始,该元素可以认为已经是被排序的了;
(2)取出下一个新元素,在已经排序的元素序列中从后向前扫描;
(3)如果该元素(已排序的)大于新元素,将该元素移动到下一个位置;
(4)重复步骤(3),直到找到已排序中小于等于新元素的位置;
(5)将新元素插入到该位置;
(6)重复步骤(2)~(5)。
2.4 动图示例
以实际代码为例:
2.5 代码实现
package ten_sort;
public class Insert_sort {
public static int[] insertSort(int[] arr) {
int i, j, temp, len = arr.length;
// 从第二个元素开始比较
// 也就是每次循环时,选定一个数,若后面的数比他大,
// 放在后面,并且将后续的比它大的数排好序;若小,放在前面,并将前序比他小的数排好序。
// i指从第二个数开始比较;j表示将比第i个元素大的全部元素排好序
for (i = 1; i < len; i++) {
temp = arr[i];
for (j = i - 1; j >= 0 && arr[j] > temp; j--) {
arr[j + 1] = arr[j];
}
arr[j + 1] = temp;//此处是交换
}
return arr;
}
public static void main(String[] args) {
int[] arr = { 3, 23, 4, 2, 1, 12, 5 };
int[] insertArray = insertSort(arr);
for (int i = 0; i < insertArray.length; i++) {
System.out.println(insertArray[i] + " !! ");
}
}
}
2.6 性能分析
简而言之:时间O(n²),空间O(1),稳定
3.希尔排序
3.1 定义
希尔排序是简单插入排序的改进版。它与插入排序的不同之处在于,他会优先比较距离较远的元素。希尔排序又叫缩小增量排序。
3.2 基本思想
改进插入排序只能移动数据一位的缺点,将整个待排序元素分割为若干子序列直接插入排序,待序列中元素基本有序后,依次插入排序。
3.3 描述
(1)选择一个增量序列T1,T2,…Tk,其中对于Ti > Tj (i > j),Tk = 1;增量因子有很多种取法:最简单的就是T(i + 1) = Ti / 2;
(2)按增量序列个数k,对序列进行k趟排序;
(3)每趟排序,根据对应的增量Ti,将待排序列分割为若干长度为m的子序列,分别对各子序列进行插入排序。仅当增量因子为1时,整个序列作为一整个序列来处理。
3.4 图示
3.5 代码
package ten_sort;
import java.util.Arrays;
public class Shell_sort {
// 插入排序改进版,又叫缩小增量排序
public static void shellSort(int[] arr) {
int incrementNum = arr.length / 3;
System.out.println("第一个增量: "+incrementNum);
while (incrementNum >= 1) {
for (int i = 0; i < arr.length; i++) {
// 进行插入排序
for (int j = i; j < arr.length - incrementNum; j = j + incrementNum) {
if (arr[j] > arr[j + incrementNum]) {
int temp = arr[j];
arr[j] = arr[j + incrementNum];
arr[j + incrementNum] = temp;
}
}
}
// 设置新的增量
// 每次除以3
incrementNum /= 3;
System.out.println("后续的增量: "+incrementNum);
}
}
public static void main(String[] args) {
// TODO Auto-generated method stub
int[] arr = { 32, 21, 53, 6, 21, 13, 64, 11, 66, 4, 211 };
shellSort(arr);
System.out.println(Arrays.toString(arr));
}
}
为真正理解增量是什么,加入输出语句。说简单一点,就是把数组分块排序,块分到越来越小,排序也随着块的缩小而完成。
3.6 分析
简而言之:时间O(nlogn),空间O(1),不稳定
4.选择排序
4.1 定义
选择排序(Select Sort):是一种简单直观的排序算法,通过不断选择序列中最大(或最小)的元素完成排序。
4.2 基本思想
在要排序的一组数中,选出最小(或者最大)的一个数与第1个位置上的元素交换位置,然后再在剩下的数中找最小(或者最大)的数与第2个位置上的元素交换位置,依次类推,直到第n-1个元素(倒数第二个元素)和第n个元素(最后一个元素)比较为止。
4.3 描述
(1)在原始序列中找到最小(或最大)元素,将其和原始序列的第一个位置上的元素进行位置交换;
(2)再从剩下的未排序元素中找到最小(或最大)元素,然后放到已排序序列的末尾(即第二个元素位置);
(3)重复步骤(2),直到所有元素均有序为止。
4.4 动态图
4.5 代码
package ten_sort;
public class Select_sortt {
public static int[] selectSort(int[] arr) {
int i, j, temp, min, len = arr.length;
// 进行i-1次大循环,最后一个元素自动有序
for (i = 0; i < len - 1; i++) {
// 找出最小元素放置于第一个位置
min = i;
for (j = i + 1; j < len; j++) {
if (arr[min] > arr[j]) {
min = j;
}
}
// 此处数的交换不应在循环中,因为寻找最小数的过程本身是一个循环,循环得出最小数才可进行排序
temp = arr[min];
arr[min] = arr[i];
arr[i] = temp;
}
return arr;
}
public static void main(String[] args) {
// TODO Auto-generated method stub
int[] arr = { 46, 42, 74, 31, 52, 73, 13, 41 };
int[] selectArray = selectSort(arr);
for (int i = 0; i < selectArray.length; i++) {
System.out.println(selectArray[i] + " ! ");
}
}
}
4.6 性能分析
简而言之:时间O(n²),空间O(1),不稳定
5.归并排序
5.1 定义
归并排序(MERGE-SORT):是建立在归并操作上的一种有效的排序算法,该算法是采用分治思想(Divided and Conquer)的一个非常典型的应用。将已经有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为二路归并。
5.2 基本思想
分治是一种解决问题的处理思想,递归是一种编程技巧。归并排序采用的就是分治思想,可以用递归代码实现。
递推公式:mergeSort(p…r) = merge(mergeSort(p…q), mergeSort(q…r))
终止条件:p >= r 时不用再继续分解
5.3 描述
(1)把长度为n的输入序列分为两个长度为n/2的子序列;
(2)对这两个子序列分别采用归并排序;
(2.1)申请空间,使其大小为两个已经排序序列之和,该空间用来存放合并后的序列;
(2.2)设定两个指针,最初位置分别为两个已经排序序列的起始位置;
(2.3)比较两个指针所指向的元素,选择相对小的元素放入合并空间,并移动指针到下一个位置;
(2.4)重复步骤(3),直到某一指针超出序列尾;
(2.5)将另一序列剩下的所有元素直接复制到合并序列尾部。
(3)将排序好的序列再拷贝回原数组
5.4 图示例
5.5 代码实现
package ten_sort;
import java.util.Arrays;
public class Merge_sort {
// 方法:递归
public static int[] mergeSort(int[] array, int low, int high) {
int mid = low + (high - low) / 2;
if (low < high) {
// 分成左边
mergeSort(array, low, mid);
// System.out.println("左边的array:" + Arrays.toString(array));
// 分成右边
mergeSort(array, mid + 1, high);
// System.out.println("右边的high:"+high);
// System.out.println("右边的array:" + Arrays.toString(array));
// 左右归并排序
merge(array, low, mid, high);
// 几个数就调用几次
System.out.println("排序过程 merge的array:" + Arrays.toString(array));
// System.out.println("排序过程:"+Arrays.toString(array));
}
return array;
}
private static void merge(int[] array, int low, int mid, int high) {
// TODO Auto-generated method stub
if (array == null || array.length == 0) {
return;
}
// 临时数组
int[] temp = new int[high - low + 1];
// 左指针
int i = low;
// 右指针
int j = mid + 1;
// 临时数组的指针
int k = 0;
// 较小数放于临时数组中
while (i <= mid && j <= high) {
// 此处必须有<=,若无=,则排序不稳定,相同时,左边元素先插入
if (array[i] <= array[j]) {
// 将array[i]放在 temp[k]处
temp[k++] = array[i++];
} else {
temp[k++] = array[j++];
}
}
// 当i <= mid时,说明左边序列中元素有剩余,则把剩余的全部元素移动到临时数组
while (i <= mid) {
temp[k++] = array[i++];
}
// 当j <= high时,说明右边序列中元素有剩余,则把剩余的全部元素移动到临时数组
while (j <= high) {
temp[k++] = array[j++];
}
for (int m = 0; m < temp.length; m++) {
array[m + low] = temp[m];
// System.out.println("m的值:" + m);
// System.out.println("temp:" + Arrays.toString(temp));
}
}
public static void main(String[] args) {
// TODO Auto-generated method stub
int[] array = { 32, 12, 5, 2, 3, 1, 6 };
mergeSort(array, 0, array.length - 1);
System.out.println(array.length);
System.out.println(Arrays.toString(array));
}
}
上述代码排序过程:
排序过程 merge的array:[12, 32, 5, 2, 3, 1, 6]
排序过程 merge的array:[12, 32, 2, 5, 3, 1, 6]
排序过程 merge的array:[2, 5, 12, 32, 3, 1, 6]
排序过程 merge的array:[2, 5, 12, 32, 1, 3, 6]
排序过程 merge的array:[2, 5, 12, 32, 1, 3, 6]
排序过程 merge的array:[1, 2, 3, 5, 6, 12, 32]
5.6 分析
简而言之:时间O(nlogn),空间O(n),稳定
6. 快速排序
6.1 定义
快速排序(Quick Sort):是一种典型的交换排序算法,其通过不断的比较和移动来实现排序。其排序过程速度快、效率高。
6.2 基本思想
对于给定的一组元素,选择一个基准元素,通常选择第一个或者最后一个,通过一趟扫描,将序列分为两个部分,一部分比基准元素小,一部分比基准元素大,此时基准元素的的位置就是在整个序列有序后它的位置,然后用同样的方法递归地将划分的两个部分再进行上面的操作进行排序,直到序列中所有的元素都有序了为止。
快速排序的伪代码:
//快速排序,A是数组,n表示数组的大小
quickSort(A, 0, n-1){
// 快速排序递归函数,p,r为下标
if (p >= r ) return;
q = partition(A, p, r) ; // 获取基准点
quickSort(A, p, q-1); // 前半部分排序
quickSort(A, q+1, r); // 后半部分排序
}
6.3 描述
(1)选择一个基准点元素,通常选择第一个或者最后一个;
(2)然后分别从数组的两端扫描数组,设两个指示标志(low指向起始为止,high指向末尾),首先从后半部分开始扫描,扫描时high指针跟着移动,如果发现有元素比该基准点的值小,则就交换low和high位置上的元素。然后再从前半部分开始扫描,扫描时low指针跟着移动,如果发现有元素比基准点的值大时,则交换high和low位置上的元素
(3)循环(2),直到lo >= hi,然后把基准点的值放到high这个位置,一次排序就完成了;
(4)采用递归的方式分别对前半部分和后半部分进行排序,当前半部分和后半部分均有序时,整个数组自然也就有序了
6.4 图例
6.5 代码
package ten_sort;
import java.util.Arrays;
public class Quick_sort {
public static void quickSort(int[] arr, int low, int high) {
if (low >= high) {
return;
}
// 进行第一轮排序获取分割点
int index = partition(arr, low, high);
// System.out.println("high:"+high);
// 排前半部分
quickSort(arr, low, index - 1);
// 排後半部分
quickSort(arr, index + 1, high);
}
// Alt+shift+j实现注释
/** * 一次快速排序 * @param arr 数组 * @param low 数组前下标 * @param high 数组后下标 * @return key的下标index,也是分片隔点 */
private static int partition(int[] arr, int low, int high) {
// TODO Auto-generated method stub
// 固定的切分方式:选取数组前下标的元素为基准
int key = arr[low];
while (high > low) {
// 从后半部分往前
while (high > low && arr[high] >= key) {
high--;
}
// 交换位置,把后半部分比基准点位置元素值小的元素交换到前半部分的low位置处
arr[low] = arr[high];
// 从前半部分往后
while (high > low && arr[low] <= key) {
low++;
}
arr[high] = arr[low];
}
arr[high] = key;
System.out.println("分片隔点 index:" + key);
System.out.println("快排后的数组:" + Arrays.toString(arr));
return high;
}
public static void main(String[] args) {
int[] arr = { 5,3,6,7,1,2,4};
System.out.println("原数组:" + Arrays.toString(arr));
quickSort(arr, 0, arr.length - 1);
for (int i : arr) {
System.out.println(i + ",");
}
}
}
原数组:[5, 3, 6, 7, 1, 2, 4]
分片隔点 index:5
快排后的数组:[4, 3, 2, 1, 5, 7, 6]
分片隔点 index:4
快排后的数组:[1, 3, 2, 4, 5, 7, 6]
分片隔点 index:1
快排后的数组:[1, 3, 2, 4, 5, 7, 6]
分片隔点 index:3
快排后的数组:[1, 2, 3, 4, 5, 7, 6]
分片隔点 index:7
快排后的数组:[1, 2, 3, 4, 5, 6, 7]
6.6 分析
简而言之:时间O(nlogn),空间O(n),不稳定
6.7 改进
【参考及推荐阅读:快速排序算法Java详解】
快速排序的优缺点:
优点:
(1)对于当数据量很大的时候,快速排序很容易将某个元素放到对应的位置;
缺点:
(1)如果原始数组就是有序的,那么快速排序过程中对序列的划分会十分的不均匀,将序列划分为:1和n-1大小(时间复杂度为:O()),但是我们理想状态下是二分(时间复杂度为:O(nlogn))
(2)对于小数组进行排序时,也需要递归好几次才能将数据放到正确的位置上;
(3)快速排序不是稳定的排序算法,当重复数据比较多时,效率比较低。
那么下面就分别针对上面所列的三个缺点提出三种改进版的快速排序算法:
【1】三数取中法:优化分区时选取基准点pivot
找一个可能在文件的中间位置的元素作为pivot。则可以选取数组中最左边元素、最右边元素以及中间元素中中间大小的元素作为pivot,这样使得最坏情况几乎不可能再发生,其次它减少了观察哨的需要。
【2】序列较小时,使用插入排序代替快速排序
对于基本的快速排序中,当递归到后面时【分区越来越小】,程序会调用自身的许多小文件,需要递归好几次才能将数据放入到正确的位置,因而在遇到子文件时需要我们对传统的快速排序算法进行改进。一种方法就是:每次递归开始之前对文件的大小进行测试,如果小于设定值,则将调用插入排序【插入排序对小文件的排序比较好】
【3】重复元素较多时,使用三分区法
通过划分让相等的元素连续地摆放,然后只对左侧小于V的序列和右侧大于V的序列进行排序。
7. 桶排序
7.1 定义
桶排序(Bucket Sort):又被称为:箱排序,是非基于比较的排序算法,因此其时间复杂度是线性的,为O(n)。
7.2 算法描述
(1)设置一个定量的数组作为空桶;
(2)遍历数列,并且把数据元素挨个放到对应的桶中;
(3)对每个不是空的桶子进行排序;
(4)从不是空的桶子里把项目再放回原来的序列里。
7.3 图例
7.4 代码
package ten_sort;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
public class Bucket_sort {
public static List bucketSort(int[] arr) {
if (arr.length == 0 || arr == null) {
return null;
}
int max = 0, min = 0;
// 找数组最大最小值
for (int i = 0; i < arr.length; i++) {
max = Math.max(max, arr[i]);
min = Math.min(min, arr[i]);
}
// 计算桶数
int bucketCount = (max - min) / arr.length + 1;
// 创建桶数组
ArrayList<ArrayList<Integer>> bucketArray = new ArrayList<ArrayList<Integer>>(bucketCount);
for (int i = 0; i < bucketCount; i++) {
bucketArray.add(new ArrayList<Integer>());
}
// 把数据放入桶中
for (int i = 0; i < arr.length; i++) {
bucketArray.get((arr[i] - min) / arr.length).add(arr[i]);
}
// 对每个桶的数据进行排序
for (int i = 0; i < bucketArray.size(); i++) {
Collections.sort(bucketArray.get(i));
}
return bucketArray;
}
public static void main(String[] args) {
// TODO Auto-generated method stub
int arr[] = { 4, 3, 6, 7, 3, 8, 3, 2 };
ArrayList list = (ArrayList) bucketSort(arr);
System.out.println(list.toString());
}
}
7.4 分析
时间:O(n); 空间:O(n); 稳定
7.5 适用场景
桶排序对要排序的数据的要求还是十分苛刻的。
首先,要排序的n个数据要很容易的划分到m个桶里,并且桶与桶之间有着天然的大小顺序。这样每个桶内的数据都排序完成后,桶与桶之间的数据不需要再进行排序。
其次,数据在各个桶之间的分布是比较均匀的。如果数据经过桶划分之后,有些桶里面的数据非常多,有些非常少,很不平均,那桶内数据的时间复杂度就不再是常量级了。在极端情况下,如果数据都被划分到一个桶里,那就退化为O(nlogn)的排序算法了。
从上面的分析可以看出来,桶排序比较适合在外部排序中,所谓的外部排序就是数据存储在外部磁盘中,数据量比较大,内存有限,无法将数据全部加载到内存中。
比如说我们有10GB的订单数据,我们希望按订单金额进行排序,但是我们的内存有限,只有几百MB,没有办法一次性将10GB的数据全都加载到内存中,这个时候我们就可以借助桶排序来解决这个问题了。我们可以先扫描一遍文件,看订单金额所处的数据范围。假设经过扫描之后我们得到订单金额最小是1元,最大是10万元。我们将所有订单根据金额划分到100个桶里,第一个桶我们存储金额在1元到1000元之间的订单,第二个桶存储金额在1001元到2000元之内的订单,以此类推。每一个桶对应一个文件,并且按照金额范围的大小顺序编号命名(00,01,02 … 99)。
理想的情况下,如果订单金额在1到10万之间均匀分布,那订单会被均匀划分到100个文件中,每个小文件中大约存储100MB的订单数据,我们就可以将这100个小文件依次放到内存中,再用快速排序来排序,等所有文件都排好序之后,我们只需要按照文件编号,从小到大依次读取每个文件中的订单数据,并将其写入一个文件中,那这个文件中存储的就是按照金额从小到大的订单数据了。
不过你可能也发现,订单按照金额在1元到10万元之间并不一定是均匀的,所以10GB订单数据是无法均匀地划分到100个文件中的。有可能某个金额区间的数据特别多,划分之后对应的文件就会很大,没法一次性读入内存。那么针对这些划分之后还是比较大的文件,我们可以继续划分,比如:订单金额在1到1000元的数据比较多,我们就将这个区间继续划分为10个小区间,1~ 100,101~ 200 … 901~ 1000元。如果划分之后,101~200元之间的订单还是太多,无法一次读入内存,那就再继续进行划分,直到所有的文件都能读入到内存为止。
【ps:桶排序适用场景内容参考自极客时间的《数据结构与算法之美》专栏】
8.计数排序
8.1 定义
计数排序:不是基于比较的排序算法,其核心在于将输入的数据值转化为键存储在额外开辟的数组空间中。作为一种线性时间复杂度的排序,计数排序要求输入的数据必须是有确定范围的整数。
8.2 场景描述
计数排序只能用在数据范围不大的场景中,如果数据范围k要比排序的数据n大很多,就不适合用计数排序了。而且计数排序只能给非负整数排序,如果排序的数据是其他数据类型,要将其在不改变相对大小的情况下,转化为非负整数。
比如:如果考生的成绩精确到小数点后一位,我们就需要将所有分数都先乘以10,转化为整数,然后再放到9010个桶内。再比如,如果要排序的数据中有分数,数据范围是[-1000, 1000],那么我们就需要先对每个数据都加1000,转化为非负整数
8.3 描述
(1)找出待排序的数组中最大和最小的元素;
(2)统计数组中每个值为i的元素出现的次数,存入数组的C的第i项;
(3)对所有的计数累加(从C中的第一个元素开始,每一项和前一项相加);
(4)反向填充目标数组,将每个元素i放在新数组的第C[ i ]项,每放一个元素就将c[ i ]减去1。
8.4 图示例
8.5 代码
package ten_sort;
import java.util.Arrays;
public class Count_sort {
public static void countSort(int[] a, int n){
if(n <= 1){
return;
}
// 查找数组中数据的范围
int max = a[0];
for(int i = 1; i < n; i++){
if(max < a[i]){
max = a[i];
}
}
int[] c = new int[max + 1]; // 申请一个计数数组C,下标大小为[0, max]
for(int i = 0; i <= max; i++){
c[i] = 0; // 将数组c中的元素都初始化为0
}
// 计算每个元素的个数,放入c中
for(int i = 0; i < n; i++){
c[a[i]]++;
}
// 依次累加
for(int i = 1; i <= max; i++){
c[i] = c[i - 1] + c[i]; // c数组当前下标的值等于当前下标对应值的数目和前面所有值数目之和
}
// 临时数组r,存储排序之和的结果
int[] r = new int[n];
// 计算排序的关键步骤
for(int i = n - 1; i >= 0; i--){
int index = c[a[i]] - 1;
r[index] = a[i];
c[a[i]]--;
}
// 将结果拷贝给数组a
for(int i = 0; i < n; i++){
a[i] = r[i];
}
}
// 测试
public static void main(String[] args) {
int[] a = {2, 5, 3, 0, 2, 3, 0, 3};
countSort(a, 8);
System.out.println(Arrays.toString(a));
}
}
8.6 分析
时间:O(n+k);空间:O(n+k);稳定
9.基数排序
9.1 定义
基数排序(Radix Sort):是一种非比较型整数排序算法,其原理是将字符串按位数切割成不同的单个字符,然后按每个位数分别比较。
9.2 基本思想及使用场景
基本思想:基数排序是按照低位优先排序,然后收集;再按照高位排序,然后再收集;依次类推,直到最高为。有时候有些属性是有优先级顺序的,先按低优先级排序,再按高优先级排序。最后的次序就是高优先级的在前,高优先级相同的低优先级高的在前。
应用场景:
假设我们要对10w个手机号进行从小到大的排序,我们可以使用时间复杂度为O(nlogn)。但是桶排序和计数排序就不太适用了,因为手机号11位,范围太大了。那么另外一种时间复杂度也为O(n)的基数排序就可以很好的解决这个问题。
手机号从小到大排序问题的规律:假设要比较连个手机号Num1和Num2的大小,如果在前面几位中,Num1的手机号码已经比Num2的号码大了,那么后面的几位就没有必要再进行比较了。
因此,我们可以先按照最后一位来排序手机号码,然后,再按照倒数第二位重新排序,依次类推,最后按照第一位重新排序,经过11次排序之和,手机号码就有序了。需要注意的是,这里按照每位来排序的算法必须是稳定的,因为最后一次排序只会考虑最高位的大小顺序,完全不管其他位的大小关系,那么低位排序就没有意义了。
根据每一位来排序,我们可以借助桶排序或者计数排序,因为它们都是稳定的,它们的时间复杂度都为O(n)。如果要排序的数据有k位,那我们就需要k次桶排序或者计数排序,总的时间复杂度为O(k*n)。当k不大的时候,比如手机号11位,所以基数排序的时间复杂度就近似于O(n)。
实际上,有时候要排序的数据并不是都等长的,比如我们排序牛津字典中的20w个英文单词,最短的只有1个字母,最长的是尘肺病,45个字母。对于这种不等长的数据,我们可以把所有的单词补齐到相同的长度,位数不够的可以在后面补“0”,因为补0不会影响到原有的大小顺序。
总结:基数排序对要排序的数据是有要求的,需要可以分割出独立的“位”来比较,而且位之间有递进关系,如果a数据的高位比b数据大,那剩下的低位就不用再比较了。除此之外,每一位的数据范围都不能太大,要可以用线性排序算法来排序,否则,基数排序的时间复杂度就无法做到O(n)了。
9.3 描述
(1)根据待排序整数序列的进制d(十进制为10,十六进制为16…)设置d个桶,编号分别为0,1,…,d-1;
(2)各个记录按照其关键字最低位的值的大小放入到相应的桶中;
(3)按照桶编号从小到大的顺序收集各个桶中的数据,对于同一桶中的数据按照先后次序收集,先进桶先收集;
(4)按照关键字的次低位,重复上述步骤…(没有高位的数据则高位补0 )按增量序列个数k,对序列进行k 趟排序; 每趟排序,根据对应的增量ti,将待排序列分割成若干长度为m 的子序列,分别对各子表进行直接插入排序。仅增量因子为1 时,整个序列作为一个表来处理,表长度即为整个序列的长度。
9.4 图示
9.5 代码实现
public class RadixSort {
/** * 基数排序 */
public static void RadixSort(int[] arr, int maxDigit) {
if (arr == null || arr.length == 0) {
return;
}
int mod = 1; //n取1,10,100...
int k = 0; //当把桶中的数据取出到arr时使用
int len = arr.length;
int[][] bucket = new int[10][len]; //桶,用来装每个数据
int[] count = new int[10]; //用来记每个桶中有几个数据
while (mod < maxDigit) {
//把当前的数据按照顺序存到桶中
for (int i : arr) {
int digit = (i / mod) % 10;
bucket[digit][count[digit]] = i;
count[digit]++;
}
//一次排序完,则把数据从桶中放回数组中,
for (int i = 0; i < count.length; i++) {
if (count[i] != 0) { //如果这个桶有数据,则取出
for (int j = 0; j < count[i]; j++) {
arr[k++] = bucket[i][j];
}
}
//每遍历完一个桶,把计数器归零
count[i] = 0;
}
mod *= 10; //继续比较下一位
k = 0;
}
}
public static void main(String[] args) {
int[] arr = {5555, 5, 3, 7, 2, 999, 4, 1};
RadixSort(arr, 10000);
for (int i : arr) {
System.out.print(i + " ");
}
}
}
9.6 分析
时间:O(2kn);空间:O(nd)【d为进制】;稳定
10.堆排序
10.1 定义
堆排序(Heapsort)是指利用堆这种数据结构所设计的一种排序算法。堆积是一个近似完全二叉树的结构,并同时满足堆积的性质:即子结点的键值或索引总是小于(或者大于)它的父节点。
10.2 算法描述
将初始待排序关键字序列(R1,R2….Rn)构建成大顶堆,此堆为初始的无序区;
将堆顶元素R[1]与最后一个元素R[n]交换,此时得到新的无序区(R1,R2,……Rn-1)和新的有序区(Rn),且满足R[1,2…n-1]<=R[n];
由于交换后新的堆顶R[1]可能违反堆的性质,因此需要对当前无序区(R1,R2,……Rn-1)调整为新堆,然后再次将R[1]与无序区最后一个元素交换,得到新的无序区(R1,R2….Rn-2)和新的有序区(Rn-1,Rn)。不断重复此过程直到有序区的元素个数为n-1,则整个排序过程完成。
10.3 图例
10.4 代码
package ten_sort;
import java.util.Arrays;
public class Heap_sort {
public static void heapSort(int[] arr) {
if (arr == null || arr.length == 0) {
return;
}
// 构造大顶堆,对最后一个非叶子节点开始到根节点为止排序,使数组构成大顶堆
for (int i = arr.length / 2 - 1; i >= 0; i--) {
// 调用堆调整排序
headAdjust(arr, arr.length, i);
}
// 排序完成,大顶堆构成,再将根节点和最后一个元素、最后二、三等等个元素一一交换位置比较
for (int i = arr.length - 1; i >= 1; i--) {
swap(arr, 0, i);
// 交换后,根节点最大,进行堆调整,使根节点依旧最大,才可取出
headAdjust(arr, i, 0);
// 此时由于数组已经排出了一个最大元素放在最后,所以我们 只对剩下的length--进行排序,故长度为i
}
}
private static void swap(int[] arr, int i, int i2) {
// TODO Auto-generated method stub
int temp = arr[i];
arr[i] = arr[i2];
arr[i2] = temp;
}
/** * @param arr 排序的数组 * @param length 数组长度 * @param i 需要排序的结点的索引 * <p> * 把要调整的结点i当作根节点,进行调整 */
private static void headAdjust(int[] arr, int length, int i) {
// TODO Auto-generated method stub
if (arr == null || arr.length == 0) {
return;
}
// 堆可以用数组来表示,这是因为堆是完全二叉树,
// 而完全二叉树很容易就存储在数组中。位置 k 的节点的父节点位置为 k/2,
// 而它的两个子节点的位置分别为 2k 和 2k+1
int root = i, index = 2 * root + 1;
// index直接指向root直接相连结点中的最大值
// 有左结点
while (index < length) {
// 有右结点
if (index + 1 < length) {
if (arr[index] < arr[index + 1])
index = index + 1;
}
// 此处得root的孩子结点的最大值的索引
if (arr[index] > arr[root]) {
swap(arr, index, root);
}
// 此处交换完后继续把被交换的子结点变成父节点进行调整,因为交换操作可能破坏堆
root = index;
index = 2 * root + 1;
}
}
public static void main(String[] args) {
// TODO Auto-generated method stub
int[] arr = { 5, 3, 6, 4, 1, 7, 2, 1 };
heapSort(arr);
System.out.println(Arrays.toString(arr));
}
}
10.5 分析
时间:O(nlogn) 空间:O(1) ;不稳定