1. 数组的概述
- 数组(Array), 是多个相同类型数据按一定顺序排列
的集合, 并使用一个名字命名, 并通过编号的方式
对这些数据进行统一管理 - 数组的常见概念
- 数组名
- 下标(或索引)
- 元素
- 数组的长度
- 数组本身是引用数据类型, 而数组中的元素可以是任何数据类型, 包括基本数据类型和引用数据类型
- 创建数组对象会在内存中开辟一整块连续的空间, 而数组名中引用的是这块连续空间的首地址
- 数组的长度一旦确定, 就不能修改
- 我们可以直接通过下标(或索引)的方式调用指定位置的元素, 速度很快
- 数组的分类
- 按照维度:一维数组、 二维数组、 三维数组、 …
- 按照元素的数据类型分:基本数据类型元素的数组、 引用数据类型元素的数组(即对象数组)
2. 一维数组
2.1 声明
type var[] 或 type[] var;
2.2 初始化
- 动态初始化:数组声明且为数组元素分配空间与赋值的操作分开进行
int[] arr = new int[3];
arr[0] = 3;
arr[1] = 9;
arr[2] = 8; - 静态初始化:在定义数组的同时就为数组元素分配空间并赋值
int arr1[] = new int[]{3,3,3};
int[] arr2 = new int[]{3,3,3};
int arr3[] = {3,3,3};
int[] arr4 = {3,3,3};
int arr5[];
arr5 = new int[]{3,3,3};
int[] arr6;
arr6 = new int[]{3, 3, 3};
2.3 引用
-
定义并用运算符new为之分配空间后,才可以引用数组中的每个元素
-
数组元素的引用方式:数组名[数组元素下标]
- 数组元素下标可以是整型常量或整型表达式。如a[3] , b[i] , c[6*i]
- 数组元素下标从0开始;长度为n的数组合法下标取值范围: 0 —>n-1; 如int a[]=new int[3]; 可引用的数组元素为a[0]、 a[1]、 a[2]
-
每个数组都有一个属性length指明它的长度,例如: a.length 指明数组a的长度(元素个数)
- 数组一旦初始化,其长度是不可变的
2.4 初始化默认值
- 数组元素的默认初始化值
- 对于基本数据类型而言,默认初始化值各有不同
- 对于引用数据类型而言,默认初始化值为null(注意与0不同!)
2.5 内存解析
3. 多维数组
- Java 语言里提供了支持多维数组的语法
- 如果说可以把一维数组当成几何中的线性图形,那么二维数组就相当于是一个表格,像右图Excel中的表格一样
- 对于二维数组的理解,我们可以看成是一维数组array1又作为另一个一维数组array2的元素而存在。其实, 从数组底层的运行机制来看,其实没有多维数组
3.1 初始化
3.2 内存解析
4. 数组中涉及到的常见算法
4.1 数组元素的赋值(杨辉三角、回形数等)
4.1.1 杨辉三角
@Test
public void MyTest01() {
Scanner scanner = new Scanner(System.in);
//输入数字控制杨辉三角的层数
int layer = scanner.nextInt();
//利用二维数组的结构实现(容易赋值)
int[][] triangle = new int[layer][layer];
//外层循环控制行的赋值
for (int i = 0; i < triangle.length; i++) {
//当i=0,j按照步长为1往后赋值(每一行数字的个数等于行数,所以循环次数就是i)
for (int j = 0; j <= i; j++) {
//每行第一个数跟最后一个数都是1
if (j == 0 || i == j) {
triangle[i][j] = 1;
} else {
//杨辉三角的规律
triangle[i][j] = triangle[i - 1][j - 1] + triangle[i - 1][j];
}
}
}
for (int i = 0; i < triangle.length; i++) {
for (int j = 0; j < triangle.length - i; j++) {
//*替换成" "
System.out.print(" ");
}
//替换的同时输出杨辉三角
for (int k = 0; k <= i; k++) {
System.out.print(triangle[i][k] + " ");
}
//保证换行
System.out.println();
}
}
4.1.2 回形数组
@Test
public void MyTest02() {
/**
* 回型数组
*/
Scanner scanner = new Scanner(System.in);
int length = scanner.nextInt();
if (length < 0) {
throw new RuntimeException("参数extent不能<0");
}
int index = 1;//用于给数组赋值
int rightLength = length - 1; //数组右边界限定值
int bottomLength = length - 1; //数组下边界限定值
int leftLength = 0; //数组左边界限定值
int topLength = 0; //数组上边界限定值
//创建一个指定容量的二维数组
int[][] arr = new int[length][length];
//如果左边界<=右边界就执行循环体
while (leftLength <= rightLength) {
//上边赋值:从左边界开始,右边界结束,并且上边界加一
for (int i = leftLength; i <= rightLength; i++) {
arr[topLength][i] = index++;
}
topLength++; //上边界加一
//右边赋值:从上边界开始,下边界结束,并且右边界减一
for (int j = topLength; j <= bottomLength; j++) {
arr[j][rightLength] = index++;
}
rightLength--;
//下别赋值:从右边界开始,下边界结束,并且下边界减一
for (int i = rightLength; i >= leftLength; i--) {
arr[bottomLength][i] = index++;
}
bottomLength--;
//左边赋值:从下边界开始,上边界结束,并且左边界加一
for (int j = bottomLength; j >= topLength; j--) {
arr[j][leftLength] = index++;
}
leftLength++;
}
for (int[] anInt : arr) {
for (int i : anInt) {
System.out.print(i + "\t");
}
System.out.println();
}
System.out.println("\n");
}
4.2 求数值型数组中元素的最大值、最小值、平均数、总和等
略
4.3 数组的复制、反转、查找(线性查找、二分法查找)
4.3.1 线性查找
@Test
public void MyTest03() {
System.out.println("please input numbers count:");
Scanner scan = new Scanner(System.in);
int count = scan.nextInt();
int[] numbers = new int[count];
System.out.println("please input numbers:");
for (int i = 0; i < numbers.length; i++) {
numbers[i] = scan.nextInt();
}
System.out.println("please input you want to find number:");
int target = scan.nextInt();
boolean find = false;
for (int i = 0; i < numbers.length; i++) {
if (numbers[i] == target) {
System.out.println("we find " + target + ", it is " + (i + 1) + " number.");
find = true;
break;
}
}
if (!find) {
System.out.println("sorry ,we can not find " + target);
}
scan.close();
// please input numbers count:
// 5
// please input numbers:
// 5
// 6
// 7
// 9
// 10
// please input you want to find number:
// 9
// we find 9, it is 4 number.
// please input numbers count:
// 5
// please input numbers:
// 5
// 6
// 7
// 9
// 10
// please input you want to find number:
// 11
// sorry ,we can not find 11
}
4.3.2 二分查找法
public void MyTest04() {
int[] arr3 = new int[]{-99, -54, -2, 0, 2, 33, 43, 256, 999};
boolean isFlag = true;
int number = 256;
//int number = 25;
int head = 0;//首索引位置
int end = arr3.length - 1;//尾索引位置
while (head <= end) {
int middle = (head + end) / 2;
if (arr3[middle] == number) {
System.out.println("找到指定的元素,索引为: " + middle);
isFlag = false;
break;
} else if (arr3[middle] > number) {
end = middle - 1; //中间索引位置的上一个位置
} else {//arr3[middle] < number
head = middle + 1; //中间索引位置的下一个位置
}
}
if (isFlag) {
System.out.println("未找打指定的元素");
}
}
4.4 数组元素的排序算法
以下借鉴:
十大排序算法总结-https://zhuanlan.zhihu.com/p/378430869
- 排序算法分为外部排序和内部排序。
- 内部排序是指数据记录在内存中进行排序
- 外部排序是排序的数据量很大,一次不能容纳全部的排序记录,在排序过程中需要访问外存。
常见的内部排序算法有:冒泡排序、选择排序、插入排序、希尔排序、归并排序、快速排序、堆排序、计数排序、桶排序、基数排序
而对于内部排序,又可以分为稳定排序和不稳定排序。所谓稳定排序是指排序后2个相等值的顺序和排序前的顺序一样,不稳定排序则相反(可能顺序不一样,不是必然的)
-
稳定的排序算法有:冒泡排序、插入排序、归并排序、计数排序、桶排序、基数排序。
-
不稳定的排序算法有:选择排序、希尔排序、快速排序、堆排序。
衡量排序法优劣的三个方面:
- 时间复杂度:主要分析关键字的比较次数和记录的移动次数
- 空间复杂度:分析排序算法中需要多少辅助内存
- 稳定性:若两个记录A和B的关键字值相等,但排序后A、B的先后次序保持不变,则称这种排序算法是稳定的;反之就是不稳定的
In-place表示占用常数内存,不占用额外内存。
Out-place表示占用额外内存。
4.4.1 冒泡排序
- 说明
冒泡排序(Bubble Sort),正如它的名字一样,冒泡,每次排序都会把最大的元素移到列表的末尾 - 算法步骤
- 两两比较相邻的元素,如果前面一个比后面一个大, 则交换它们的位置(稳定排序)
- 对每一对相邻的元素重复上面的步骤,直到列表末尾,此时最大的数放到了最后
- 针对所有的元素重复上面两步,直到没有元素需要交换
- 动图演示
- 最好的情况
输入的数组本来就是有序的,此时只需要遍历一次即可。时间复杂度O(n) - 最坏的情况
输入的数组是降序的,此时时间复杂度是O(n ^ 2) - 编码实现
import java.util.Arrays;
public class Test {
public static void main(String[] args) {
int[] arr = {10, 87, 92, 7, 40, 26, 68, 102, 863, 83};
int[] res = bubbleSort(arr);
Arrays.stream(res).forEach(System.out::println);
}
public static int[] bubbleSort(int[] arr) {
// 拷贝原数组,不改变原数组的内容
int[] array = Arrays.copyOf(arr, arr.length);
// 比较n - 1轮
for (int i = 1; i < array.length; i++) {
boolean flag = true;
// 末尾的已经排好序,不需要再比
for (int j = 0; j < array.length - i; j++) {
if (array[j + 1] < array[j]) {
int temp = array[j + 1];
array[j + 1] = array[j];
array[j] = temp;
flag = false;
}
}
// 如果flag为true。说明这轮没有元素交换,此时数组已经有序
if (flag) {
break;
}
}
return array;
}
}
此处使用可一个flag做标志位,在每轮循环进行时,初始化为true,如果有元素进行了交换,则置为false,结束该轮循环时,判断flag是否为true,如果是,说明该轮循环没有进行元素交换,数组已经有序,直接break。
总结:冒泡排序是稳定排序算法,最好时间复杂度为O(n),最坏时间复杂度为O(n ^ n),平均时间复杂度为O(n ^ n),空间复杂度为O(1),这里我们复制了原数组,空间复杂度是O(n)
4.4.2 选择排序
- 说明
选择排序,在任何情况下时间复杂度都是O(n ^ n),所以适用于数据量比较小的数组排序 - 算法步骤
- 对于未排序的子数组,找出子数组中最小(最大)的元素,然后把该元素放到已排序子数组的末尾
- 重复上面的步骤,直到数组末尾
- 动图演示
- 编码实现
import java.util.Arrays;
public class Test {
public static void main(String[] args) {
int[] arr = {10, 87, 92, 7, 40, 26, 68, 102, 863, 83};
int[] res = selectSort(arr);
Arrays.stream(res).forEach(System.out::println);
}
public static int[] selectSort(int[] arr) {
// 比较n - 1轮
for (int i = 0; i < arr.length - 1; i++) {
int min = i;
// 找出[i + 1, n]最小的元素
for (int j = i + 1; j < arr.length; j++) {
if (arr[j] < arr[min]) {
min = j;
}
}
// 把最小的元素放到未排序部分的头部
if (min != i) {
int temp = arr[min];
arr[min] = arr[i];
arr[i] = temp;
}
}
return arr;
}
}
总结:选择排序是不稳定排序算法,最好时间复杂度为O(n ^ n),最坏时间复杂度为O(n ^ n),平均时间复杂度为O(n ^ n),空间复杂度为O(1)
4.4.3 插入排序
-
说明
插入排序是一种简单直观的排序算法,它的原理是构建有序序列,对于未排序的序列,每次拿一个元素在已排序序列中从后往前扫描寻找合适的位置插入 -
算法步骤
- 从第一个元素开始,该元素可以认为已排序
- 取出下一个元素,在已排序序列中从后往前扫描,如果已排序序列中的元素大于该元素,则把序列中的元素往后移
- 重复第(2)步,直到找到已排序的元素小于等于该元素的位置,将该元素插入该位置后面
- 重复(2)(3)两步
-
动图演示
-
编码实现
import java.util.Arrays;
public class Test {
public static void main(String[] args) {
int[] arr = {10, 87, 92, 7, 40, 26, 68, 102, 863, 83};
int[] res = insertSort(arr);
Arrays.stream(res).forEach(System.out::println);
}
public static int[] insertSort(int[] arr) {
for (int i = 1; i < arr.length; i++) {
int j, temp = arr[i];
// 在已排序序列中从后往前比较
for (j = i - 1; j >= 0; j--) {
// 找到插入位置
if (arr[j] <= temp) {
break;
}
// 元素往后移
arr[j + 1] = arr[j];
}
arr[j + 1] = temp;
}
return arr;
}
}
总结:插入排序是稳定排序算法,最好时间复杂度为O(n),最坏时间复杂度为O(n ^ n),平均时间复杂度为O(n ^ n),空间复杂度为O(1)
4.4.4 快速排序
-
说明
快速排序采用分治法来把一个串行(list)分成两个子串(sub-list),本质上来看,快速排序应该算是在冒泡排序基础上的递归分治法。
快速排序的最坏运行情况是 O(n²),比如说顺序数列的快排。但它的平摊期望时间是 O(nlogn),且 O(nlogn) 记号中隐含的常数因子很小,比复杂度稳定等于 O(nlogn) 的归并排序要小很多。所以,对绝大多数顺序性较弱的随机数列而言,快速排序总是优于归并排序。 -
算法步骤
- 从数列中挑出一个元素,称为基准(pivot)
- 将数列中比基准小的元素放在基准的左边,比基准大的元素放在基准的右边(相同的数可以放到任一边)。在这个分区退出之后,该基准就处于数列的中间位置,这个操作称为分区(partition)操作
- 递归(recursive)把小于基准元素的序列和大于基准元素的序列进行排序
-
动图演示
-
编码实现
import java.util.Arrays;
public class Test {
public static void main(String[] args) {
int[] arr = {10, 87, 92, 7, 40, 26, 68, 102, 863, 83};
int[] res = quickSort(arr, 0, arr.length - 1);
Arrays.stream(res).forEach(System.out::println);
}
public static int[] quickSort(int[] arr, int left, int right) {
if (left < right) {
int partition = partition(arr, left, right);
quickSort(arr, left, partition - 1);
quickSort(arr, partition + 1, right);
}
return arr;
}
// 分区操作
private static int partition(int[] arr, int left, int right) {
// 设定基准值
int pivot = left;
int index = pivot + 1;
for (int i = index; i <= right; i++) {
// 小于基准值的元素放到左边
if (arr[i] < arr[pivot]) {
swap(arr, index, i);
index++;
}
}
// 移动基准值的位置
swap(arr, index - 1, pivot);
return index - 1;
}
// 交换两个元素
private static void swap(int[] arr, int i, int j) {
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
}
总结:快速排序是不稳定排序算法,最好时间复杂度为O(nlogn),最坏时间复杂度为O(n ^ n),平均时间复杂度为O(nlogn),空间复杂度为O(logn)
4.4.5 希尔排序
-
说明
希尔排序(shell sort)也称递减增量排序算法,是插入排序的一种更高效的算法,但它是不稳定排序算法。它与插入排序的不同之处在于,它会优先比较距离较远的元素
希尔排序是基于插入排序的以下两点性质而提出改进方法的:
(1)插入排序在对几乎已经有序的序列进行排序时,可以达到线性排序的效率。
(2)但插入排序一般是低效的,因为插入排序每次只能将数据移动一位。
希尔排序的基本思想是:先将整个待排序的序列分成若干个子序列进行直接插入排序,待整个序列中的记录基本有序时,再对全体记录进行依次直接插入排序 -
算法步骤
先将整个待排序的记录序列分割成为若干子序列分别进行直接插入排序,具体算法描述- 选择一个增量序列t1,t2,…,tk,其中ti>tj,tk=1;
- 按增量序列个数k,对序列进行k 趟排序;
- 每趟排序,根据对应的增量ti,将待排序列分割成若干长度为m 的子序列,分别对各子表进行直接插入排序。仅增量因子为1 时,整个序列作为一个表来处理,表长度即为整个序列的长度
-
动图演示
-
编码实现
import java.util.Arrays;
public class Test {
public static void main(String[] args) {
int[] arr = {10, 87, 92, 7, 40, 26, 68, 102, 863, 83};
int[] res = shellSort(arr);
Arrays.stream(res).forEach(System.out::println);
}
public static int[] shellSort(int[] arr) {
int length = arr.length;
int temp;
for (int step = length / 2; step >= 1; step /= 2) {
for (int i = step; i < length; i++) {
temp = arr[i];
int j = i - step;
while (j >= 0 && arr[j] > temp) {
arr[j + step] = arr[j];
j -= step;
}
arr[j + step] = temp;
}
}
return arr;
}
}
总结:希尔排序是不稳定排序算法,最好时间复杂度为O(nlog2n),最坏时间复杂度为O(nlog2n),平均时间复杂度为O(nlogn),空间复杂度为O(1)。
希尔排序的核心在于间隔序列的设定。既可以提前设定好间隔序列,也可以动态的定义间隔序列。动态定义间隔序列的算法是《算法(第4版)》的合著者Robert Sedgewick提出的
4.4.6 归并排序
-
说明
归并排序(Merge Sort)是建立在归并操作上的一种排序算法,该算法是采用分治法(Divide and Conquer)的一个典型应用。
作为一种典型的分而治之思想,归并排序的思想有两种方法:
自上而下的递归(所有的递归都可以用迭代重写)
自下而上的迭代
在《数据结构与算法 JavaScript 描述》中,作者给出了自下而上的迭代方法。但是对于递归法,作者却认为:
However, it is not possible to do so in JavaScript, as the recursion goes too deep for the language to handle.
然而,在 JavaScript 中这种方式不太可行,因为这个算法的递归深度对它来讲太深了。
说实话,我不太理解这句话。意思是 JavaScript 编译器内存太小,递归太深容易造成内存溢出吗?还望有大神能够指教。
和选择排序一样,归并排序的性能不受输入数据的影响,但表现比选择排序好的多,因为始终都是 O(nlogn) 的时间复杂度。代价是需要额外的内存空间。 -
算法步骤
- 申请一个数组,大小为两个子数组的长度之和,用来存放合并排序后的元素
- 设定两个指针,最初位置分别为两个子数组的起始位置
- 比较两个指针所指向的位置,将小的那个元素放入合并空间,并移动该指向到下一个位置
- 重复步骤3直到一个指针到达末尾
- 将另一序列剩下的所有元素放到合并空间的末尾
-
动图演示
-
编码实现
import java.util.Arrays;
public class Test {
public static void main(String[] args) {
int[] arr = {10, 87, 92, 7, 40, 26, 68, 102, 863, 83};
int[] res = mergeSort(arr);
Arrays.stream(res).forEach(System.out::println);
}
public static int[] mergeSort(int[] arr) {
if (arr.length <= 1) {
return arr;
}
int mid = arr.length / 2;
int[] left = Arrays.copyOfRange(arr, 0, mid);
int[] right = Arrays.copyOfRange(arr, mid, arr.length);
return merge(mergeSort(left), mergeSort(right));
}
/**
* 将两个数组有序的进行合并
* @param left
* @param right
* @return
*/
private static int[] merge(int[] left, int[] right) {
// 存放合并后的数组
int[] res = new int[left.length + right.length];
int i = 0;
while (left.length > 0 && right.length > 0) {
if (left[0] > right[0]) {
res[i++] = right[0];
right = Arrays.copyOfRange(right, 1, right.length);
} else {
res[i++] = left[0];
left = Arrays.copyOfRange(left, 1, left.length);
}
}
// 剩下的元素直接放到res的末尾
while (left.length > 0) {
res[i++] = left[0];
left = Arrays.copyOfRange(left, 1, left.length);
}
while (right.length > 0) {
res[i++] = right[0];
right = Arrays.copyOfRange(right, 1, right.length);
}
return res;
}
}
总结:归并排序是稳定排序算法,最好时间复杂度为O(nlogn),最坏时间复杂度为O(nlogn),平均时间复杂度为O(nlogn),空间复杂度为O(n)。
和选择排序一样,归并排序的性能不受输入数据的影响,但表现比选择排序好的多,因为始终都是O(nlogn)的时间复杂度。代价是需要额外的内存空间。
4.4.7 堆排序
-
说明
堆排序(Heap Sort)是指利用堆这种数据结构所设计的一种排序算法。堆积是一个近似完全二叉树的结构,并同时满足堆积的性质:即子结点的键值或索引总是小于(或者大于)它的父节点。堆排序可以说是一种利用堆的概念来排序的选择排序。分为两种方法:
\qquad 大顶堆:每个节点的值都大于或等于其子节点的值,在堆排序算法中用于升序排列;
\qquad 小顶堆:每个节点的值都小于或等于其子节点的值,在堆排序算法中用于降序排列;
堆排序的平均时间复杂度为 Ο(nlogn) -
算法步骤
- 创建一个堆 H[0……n-1];
- 把堆首(最大值)和堆尾互换;
- 把堆的尺寸缩小 1,并调用 shift_down(0),目的是把新的数组顶端数据调整到相应位置;
- 重复步骤 2,直到堆的尺寸为 1。
-
动图演示
-
编码实现
import java.util.Arrays;
public class Test {
public static void main(String[] args) {
int[] arr = {10, 87, 92, 7, 40, 26, 68, 102, 863, 83};
int[] res = heapSort(arr);
Arrays.stream(res).forEach(System.out::println);
}
public static int[] heapSort(int[] arr) {
int len = arr.length;
buildMaxHeap(arr, len);
for (int i = len - 1; i > 0; i--) {
swap(arr, 0, i);
len--;
heapify(arr, 0, len);
}
return arr;
}
private static void buildMaxHeap(int[] arr, int len) {
for (int i = (int) Math.floor(len / 2); i >= 0; i--) {
heapify(arr, i, len);
}
}
private static void heapify(int[] arr, int i, int len) {
int left = 2 * i + 1;
int right = 2 * i + 2;
int largest = i;
if (left < len && arr[left] > arr[largest]) {
largest = left;
}
if (right < len && arr[right] > arr[largest]) {
largest = right;
}
if (largest != i) {
swap(arr, i, largest);
heapify(arr, largest, len);
}
}
private static void swap(int[] arr, int i, int j) {
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
}
总结:堆排序是不稳定排序算法,最好时间复杂度为O(nlogn),最坏时间复杂度为O(nlogn),平均时间复杂度为O(nlogn),空间复杂度为O(1)。
4.4.8 计数排序
-
说明
计数排序(Counting Sort)的核心在于将输入的数据值作为键存储在额外开辟的数组空间中。作为一种线性时间复杂度的排序,计数排序要求输入的数据必须是有确定范围的整数。
当输入的元素是 n 个 0 到 k 之间的整数时,它的运行时间是 Θ(n + k)。计数排序不是比较排序,排序的速度快于任何比较排序算法。
由于用来计数的数组C的长度取决于待排序数组中数据的范围(等于待排序数组的最大值与最小值的差加上1),这使得计数排序对于数据范围很大的数组,需要大量时间和内存。
通俗地理解,例如有 10 个年龄不同的人,统计出有 8 个人的年龄比 A 小,那 A 的年龄就排在第 9 位,用这个方法可以得到其他每个人的位置,也就排好了序。当然,年龄有重复时需要特殊处理(保证稳定性),这就是为什么最后要反向填充目标数组,以及将每个数字的统计减去 1 的原因。 -
算法步骤
- 找出待排序数组中最大和最小的元素。
- 统计数组中每个值为i的元素出现的次数,存入数组C的第i项。
- 对所有的计数累加(从C中的第一个元素开始,每一项和前一项相加)。
- 反向填充目标数组:将每个元素i放在新数组的第C[i]项,每放一个元素就将C[i]减1。
-
动图演示
-
编码实现
import java.util.Arrays;
public class Test {
public static void main(String[] args) {
int[] arr = {10, 87, 92, 7, 40, 26, 68, 102, 863, 83};
int[] res = countingSort(arr);
Arrays.stream(res).forEach(System.out::println);
}
public static int[] countingSort(int[] arr) {
int maxValue = getMax(arr);
int bucketLen = maxValue + 1;
int[] bucket = new int[bucketLen];
for (int num : arr) {
bucket[num]++;
}
int index = 0;
for (int i = 0; i < bucketLen; i++) {
while (bucket[i] > 0) {
arr[index++] = i;
bucket[i]--;
}
}
return arr;
}
/**
* 获取数组中的最大元素
* @param arr
* @return
*/
private static int getMax(int[] arr) {
int max = arr[0];
for (int num : arr) {
if (num > max) {
max = num;
}
}
return max;
}
}
总结:计数排序是稳定排序算法,最好时间复杂度为O(n + k),最坏时间复杂度为O(n + k),平均时间复杂度为O(n + k),空间复杂度为O(k)。
k表示序列中最大值和最小值的差,当k不是很大并且序列比较集中时,计数排序是一个很有效的排序算法。
4.4.9 桶排序
-
说明
桶排序(Bucket Sort)是计数排序的升级版,它利用函数的映射关系,高效与否的关键就在于这个映射函数的确定。为了使桶排序更加高效,我们需要做到这两点:
\qquad 在额外空间充足的情况下,尽量增大桶的数量。
\qquad 使用的映射函数能够将输入的N个数据均匀的分布到K个桶中。
同时,对于桶中元素的排序,选择何种排序算法对于性能的影响至关重要。 -
算法步骤
- 设置一个定量的数组作为空桶。
- 遍历输入数据,并且把数据一个一个放到对应的桶里。
- 对每个不是空的桶进行排序。
- 从不是空的桶里把排好序的数据拼接起来。
-
什么时候最快
当输入的数据可以均匀的分布到每一个桶中,即一个桶里放一个元素。此时时间复杂度为O(n),n为数组的长度,但需要的空间会更大。 -
什么时候最慢
当输入的数据被分配到一个桶里。 -
示意图
元素分布在桶中
然后在每个桶中排序
-
编码实现
import java.util.Arrays;
public class Test {
public static void main(String[] args) {
int[] arr = {10, 87, 92, 7, 40, 26, 68, 102, 863, 83};
int[] res = bucketSort(arr, 5);
Arrays.stream(res).forEach(System.out::println);
}
/**
* 桶排序
* @param arr
* @param bucketSize 每个桶的容量
* @return
*/
public static int[] bucketSort(int[] arr, int bucketSize) {
if (arr.length == 0) {
return arr;
}
// 求数组最大值和最小值
int min = arr[0];
int max = arr[0];
for (int num : arr) {
if (num > max) {
max = num;
} else if (num < min) {
min = num;
}
}
// 桶的数量
int bucketCount = (max - min) / bucketSize + 1;
int[][] buckets = new int[bucketCount][0];
// 利用映射函数将数据分配到各个桶中
for (int i = 0; i < arr.length; i++) {
int index = (arr[i] - min) / bucketSize;
buckets[index] = arrAppend(buckets[index], arr[i]);
}
int arrIndex = 0;
for (int[] bucket : buckets) {
if (bucket.length <= 0) {
continue;
}
// 对每个桶进行排序,这里使用了插入排序
bucket = insertSort(bucket);
for (int value : bucket) {
arr[arrIndex++] = value;
}
}
return arr;
}
/**
* 自动扩容,并保存数据
*
* @param arr
* @param value
*/
private static int[] arrAppend(int[] arr, int value) {
arr = Arrays.copyOf(arr, arr.length + 1);
arr[arr.length - 1] = value;
return arr;
}
private static int[] insertSort(int[] arr) {
for (int i = 1; i < arr.length; i++) {
int j, temp = arr[i];
// 在已排序序列中从后往前比较
for (j = i - 1; j >= 0; j--) {
// 找到插入位置
if (arr[j] <= temp) {
break;
}
// 元素往后移
arr[j + 1] = arr[j];
}
arr[j + 1] = temp;
}
return arr;
}
}
总结:桶排序是稳定排序算法,最好时间复杂度为O(n),最坏时间复杂度为O(n ^ n),平均时间复杂度为O(n + k),空间复杂度为O(n + k)。
桶排序的时间复杂度,取决与对各个桶之间数据进行排序的时间复杂度,因为其它部分的时间复杂度都为O(n)。很显然,桶划分的越小,各个桶之间的数据越少,排序所用的时间也会越少。但相应的空间消耗就会增大。
4.4.10 基数排序
-
说明
基数排序(Radix Sort),按照低位先排序,然后收集;再按照高位排序,然后再收集;依次类推,直到最高位。有时候有些属性是有优先级顺序的,先按低优先级排序,再按高优先级排序。最后的次序就是高优先级高的在前,高优先级相同的低优先级高的在前。
基数排序 vs 计数排序 vs 桶排序
这三种排序算法都利用了桶的概念,但对桶的使用方法上有明显差异:
\qquad 基数排序:根据键值的每位数字来分配桶;
\qquad 计数排序:每个桶只存储单一键值;
\qquad 桶排序:每个桶存储一定范围的数值; -
算法步骤
- 取得数组中的最大数,并取得位数;
- arr为原始数组,从最低位开始取每个位组成radix数组;
- 对radix进行计数排序(利用计数排序适用于小范围数的特点);
-
动图演示
-
编码实现
import java.util.Arrays;
public class Test {
public static void main(String[] args) {
int[] arr = {10, 87, 92, 7, 40, 26, 68, 102, 863, 83};
int maxDigit = getMaxDigit(arr);
int[] res = radixSort(arr, maxDigit);
Arrays.stream(res).forEach(System.out::println);
}
/**
* 获取最高位数
*/
private static int getMaxDigit(int[] arr) {
int maxValue = getMaxValue(arr);
return getNumLenght(maxValue);
}
private static int getMaxValue(int[] arr) {
int maxValue = arr[0];
for (int value : arr) {
if (maxValue < value) {
maxValue = value;
}
}
return maxValue;
}
protected static int getNumLenght(long num) {
if (num == 0) {
return 1;
}
int lenght = 0;
for (long temp = num; temp != 0; temp /= 10) {
lenght++;
}
return lenght;
}
private static int[] radixSort(int[] arr, int maxDigit) {
int mod = 10;
int dev = 1;
for (int i = 0; i < maxDigit; i++, dev *= 10, mod *= 10) {
// 考虑负数的情况,这里扩展一倍队列数,其中 [0-9]对应负数,[10-19]对应正数 (bucket + 10)
int[][] counter = new int[mod * 2][0];
for (int j = 0; j < arr.length; j++) {
int bucket = ((arr[j] % mod) / dev) + mod;
counter[bucket] = arrayAppend(counter[bucket], arr[j]);
}
int pos = 0;
for (int[] bucket : counter) {
for (int value : bucket) {
arr[pos++] = value;
}
}
}
return arr;
}
/**
* 自动扩容,并保存数据
*
* @param arr
* @param value
*/
private static int[] arrayAppend(int[] arr, int value) {
arr = Arrays.copyOf(arr, arr.length + 1);
arr[arr.length - 1] = value;
return arr;
}
}
基数排序基于分别排序,分别收集,所以是稳定的。但基数排序的性能比桶排序要略差,每一次关键字的桶分配都需要O(n)的时间复杂度,而且分配之后得到新的关键字序列又需要O(n)的时间复杂度。假如待排数据可以分为d个关键字,则基数排序的时间复杂度将是O(d*2n) ,当然d要远远小于n,因此基本上还是线性级别的。
基数排序的空间复杂度为O(n+k),其中k为桶的数量。一般来说n>>k,因此额外空间需要大概n个左右。