《Java 数据结构和算法 第二版》
一、概述
1、数据结构概述
数据结构 | 优点 | 缺点 |
---|---|---|
数组 | 插入快,如果知道下标,可以快速存取 | 查找慢,删除慢,大小固定 |
有序数组 | 比无序的数组查找快 | 删除和插入慢,大小固定 |
栈 | 提供后进先出方式的存取 | 存取其他项很慢 |
队列 | 提供先进先出方式的存取 | 存取其他项很慢 |
链表 | 插入快,删除快 | 查找慢 |
二叉树 | 查找、插入、删除都快(如果树保持平衡) | 删除算法复杂 |
红黑树 | 查找、插入、删除都快。树总是平衡的 | 算法复杂 |
2-3-4 树 | 查找、插入、删除都快。树总是平衡的。类似的树对磁盘存储有用 | 算法复杂 |
哈希表 | 如果关键字已知,则存取极快。插入快 | 删除慢,如果不指定关键字则存取很慢,对存储空间使用不充分 |
堆 | 插入、删除快,对最大数据项的存取很快 | 对其他数据项存取慢 |
图 | 对现实世界建模 | 有些算法慢且复杂 |
2、数组操作效率
算法 | 时间复杂度 |
---|---|
线性查找 | O ( N ) O(N) O(N) |
二分查找 | O ( l o g N ) O(logN) O(logN) |
无序数组的插入 | O ( 1 ) O(1) O(1) |
有序数组的插入 | O ( N ) O(N) O(N) |
无序数组的删除 | O ( N ) O(N) O(N) |
有序数组的删除 | O ( N ) O(N) O(N) |
二、常见的八大排序方式
排序方式 | 规则 |
---|---|
冒泡排序 | 从左到右,比较两个元素大小 如果左边元素大,则与右边元素交换位置 如果右边元素大,则索引向右移动一个位置,继续比较下两个元素大小 这样循环完一次后,就把数组中最大的元素移到了数组末端 接着进行第二轮比较,把第二大的元素移到末尾 … 最终排序完成 外层从后向前遍历 因为内层循环一遍,就会把一个当前序列中最大的元素移到末尾 排序过的元素不再需要参与遍历,所以内层循环每执行完一次, 外层循环的索引就从后往前移动一次 外层循环的索引位置,是内层循环的终点位置 内层从前向后遍历 左边的更大,就跟右边的交换位置 in<out,in+1的最大值就是out,会比较到out位置的元素的 |
选择排序 | 外层循环从左到右,遍历 [outIdx, len-2] 的元素 outIdx从0开始 内层循环从左到右,遍历 [outIdx+1, len-1] 的元素 用一个字段记录内层遍历到的最小值的索引 内层遍历完后,把最小值与外层 outIdx 位置的元素交换位置 内外层都是从左到右,内层负责筛选出当前最小的元素 筛选的最小元素与外层当前下标元素交换位置 交换完之后,外层下标右移,内层循环获取下一个最小值 |
插入排序 | 基本思想:数组分为有序部分、无序部分两块 有序部分,初始只有1个元素,就是数组的第一个元素 遍历无序部分,由于第一个元素有序,所以从第二个元素开始遍历 取出无序部分的一个元素,与有序部分的元素从大到小比较大小 先与有序部分最大的元素比较,比该元素小,则有序元素右移一位(腾出插入空间), 继续与更小的元素比较 如果该元素比某个有序部分元素大,说明该元素应该放在该有序元素后面 |
希尔排序 | 希尔排序基于插入排序 通过 加大插入排序中元素之间的间隔,并在这些有间隔的元素中进行插入排序 先取一个正整数 d1(d1 < n),把全部记录分成 d1 个组,所有距离为 d1 的倍数的记录看成一组,然后在各组内进行插入排序 然后取 d2(d2 < d1),重复上述分组和排序操作; 直到取 di = 1(i >= 1) 位置,即所有记录成为一个组 |
二分插入排序 | 基于 插入排序,区别在于查找插入位置时使用的是二分查找的方式 二分查找方式: 定义三个参数,一个left、一个right、一个middle 总共有N个元素,当插入第i个元素 temp 时, 对前面的0~i-1 个元素进行折半 left = 0; right = i-1; middle = left + (right-left)/2; temp 跟 middle 元素比较大小 如果 temp < array[middle] 则 right = middle-1; middle = left + (right-left)/2;//计算新的中间位 如果 temp > array[middle] 则 left = middle+1; middle = left + (right-left)/2;//计算新的中间位 … 直到 left > right,说明无法再折半了 此时,left 就是 temp元素的正确排序位置 |
快速排序 | 在数据集之中,选择一个元素作为“基准”(pivot) 所有小于“基准”的元素,都移到“基准”的左边; 所有大于“基准”的元素,都移到“基准”的右边。 对“基准”左边和右边的两个子集,不断重复上面的操作 直到所有子集只剩下一个元素为止 |
基数排序 | 十进制的基数是10,二进制的基数是2 以十进制为例, 根据数据个位数的值,将所有数据分为10组。 个位为0的一组、个位为1的一组、…、个位为9的一组 然后,对这10组数据重新排列,个位为0的组排再最前、…、个位 为9的组排在最后 对第1步排序后的数据,根据数据十位数的值,将所有数据分为10组 十位为0的一组、十位为1的一组、…、十位为9的一组 然后,对这10组数据重新排列,十位为0的组排再最前、…、十位 为9的组排在最后 接着是百位…,如果没有高位,则认为高位为0, 把所有位的数字都按以上方法排序完成后,整个数组就是从小到达排序了 |
堆排序 | 把数组构造成堆,然后堆排序,构造成最大堆 根据最大堆的特征,根节点是最大元素。 根节点元素与末节点元素交换位置,末尾元素就变成了最大值了。 然后把末尾元素之外的剩余元素为一个新堆,继续按最大堆排序, 排序完后,根节点与新堆的末节点交换位置, 新堆中最大元素移到了新堆的末端,也就是最大元素的前面。 如此往复… 最终,整个数组都是有序的了。 |
归并排序 | 把 n 个记录看成 n 个长度为 1 的有序子表 进行两两归并使记录关键字有序,得到 n/2 个长度为 2 的有序子表 重复第 2 步直到所有记录归并成一个长度为 n 的有序表为止 |
对象排序 | 实现 Comparable 用于两个对象的比较判断 |
1、冒泡排序
运行慢,但是最简单。
冒泡排序,是从右侧到左侧逐渐变得有序的。
1)规则
-
从左到右,比较两个元素大小,
如果左边元素大,则与右边元素交换位置
如果右边元素大,则索引向右移动一个位置,继续比较下两个元素大小
这样循环完一次后,就把数组中最大的元素移到了数组末端
接着进行第二轮比较,把第二大的元素移到末尾
…
最终排序完成。 -
排序过的元素不再参与比较
所以外层循环从后往前遍历,内层循环完成一次,外层循环的索引前移一位
内层循环的右边界为外层循环的索引
这样排序过的元素就不会再参与比较了。
2)代码实现
public void bubbleSort(int[] array) {
if (array == null || array.length == 0) {
return;
}
int len = array.length;
/* 外层从后向前遍历
* 因为内层循环一遍,就会把一个当前序列中最大的元素移到末尾
* 排序过的元素不再需要参与遍历,所以内层循环每执行完一次,
* 外层循环的索引就从后往前移动一次
* 外层循环的索引位置,是内层循环的终点位置
*/
for (int out = len - 1; out > 1; out--) {//out > 1
//内层从前向后遍历
for (int in = 0; in < out; in++) {
/* 左边的更大,就跟右边的交换位置
* in<out,in+1的最大值就是out,会比较到out位置的元素的
*/
if(array[in] > array[in+1]) {
int temp = array[in];
array[in] = array[in+1];
array[in+1] = temp;
}
}
}
}
冒泡排序 索引 >= out 的元素总是有序的
冒泡排序,嵌套了两层循环,时间复杂度 O ( N 2 ) O(N^2) O(N2)
2、选择排序
选择排序,是从左侧到右侧逐渐变得有序的。
1)规则
-
外层循环从左到右,遍历[outIdx, len-2]的元素
outIdx从0开始 -
内层循环从左到右,遍历[outIdx+1, len-1]的元素
用一个字段记录内层遍历到的最小值的索引。
内层遍历完后,把最小值与外层outIdx位置的元素交换位置。所以,数组是从左边开始逐渐有序的。
2)代码实现
public void selectSort(int[] array) {
if (array == null || array.length == 0) {
return;
}
int minIndex;//最小值索引
int len = array.length;
/* out < len-1
* 因为内层循环 in=out+1,in的最大值是len-1,不会越界
*/
for (int out = 0; out < len - 1; out++) {
minIndex = out;
/* 由于out 最大值是 len-2
* in = out+1,所以in的最大值是 len-1
* 所以内层所有元素都会比较,不会漏的
*/
for (int in = out+1; in < len; in++) {//in = out+1,in < len
if(array[in] < array[minIndex]) {
minIndex = in;//记录最小元素的索引
}
}
//本次遍历的最小值与 out位置元素交换位置
int temp = array[out];
array[out] = array[minIndex];
array[minIndex] = temp;
}
}
选择排序,索引 <= out 的元素总是有序的。
冒泡排序,嵌套了两层循环,时间复杂度 O ( N 2 ) O(N^2) O(N2)
3、插入排序
1)规则
插入的算法基本思想是:仅有一个元素的序列总是有序的,
因此,对n个记录的序列,
可从第二个元素开始直接到第n个元素,从有序部分末尾开始,从后往前比较大小,
找到该元素在有序序列中的位置,然后执行插入操作。
2)代码实现
public void insertSort(int[] array) {
if (array == null || array.length == 0) {
return;
}
int len = array.length;
for (int out = 1; out < len; out++) {
/* 默认第一个元素是有序的,
* 所以我们从有序部分最后一个元素的右边开始取出无序元素开始遍历,
* 所以第一个取出的是 索引为1的元素
* index < out 的元素,是有序的
*/
int temp = array[out];//要用来插入的元素,用一个变量保存它的值
int in = out;//内层循环索引
/* 取出的元素temp,从有序序列末尾开始,与有序部分的元素比较大小
* 如果 该有序元素比 temp 大,则把该有序元素右移一位,
* 腾出的一个空位最后是要用来插入temp用的。
* 继续用前一个有序元素与temp比较,如果比temp大,则有序元素右移
* ...
* 直到 找到一个比 temp 小的有序元素,说名temp应该插入该元素后面
* 这时候不再需要与有序部分左边的元素比较了,结束 while 循环
* 把 temp 插入到比temp小的元素后面。
* 然后继续从无序部分取出下一个元素,与有序序列比较...
*/
while (in > 0 && array[in-1] > temp) {
/* 有序元素比temp大
* 有序元素右移
*/
array[in] = array[in-1];
--in;//temp继续与有序部分的上一个元素比较大小
}
/* 跳出while循环的条件是 有序元素比 temp小,或者已经到数组头部了
* 这时直接把temp插入该位置。
*/
array[in] = temp;
}
}
index < out 的元素,是有序的
插入排序也是两层嵌套循环,时间复杂度是 O ( N 2 ) O(N^2) O(N2)
4、对象排序
1)规则
实现 Comparable 用于两个对象的比较判断
2)代码实现
用插入排序实现对对象的排序。
//实现 Comparable 用于两个对象的比较判断
class Person implements Comparable<Person> {
private String lastName;
private String firstName;
private int age;
@Override
public int compareTo(Person o) {
if (o == null) {
return 1;
}
if (age > o.age) {
return 1;
}else if (age < o.age) {
return -1;
}
return 0;
}
}
public void insertSort(Person[] array) {
if (array == null || array.length == 0) {
return;
}
int len = array.length;
for (int out = 1; out < len; out++) {
//要用来插入的元素,用一个变量保存它
Person temp = array[out];
int in = out;//内层循环索引
while (in > 0 && array[in-1].compareTo(temp) > 0) {
array[in] = array[in-1];
--in;//temp继续与有序部分的上一个元素比较大小
}
//这时直接把temp插入该位置。
array[in] = temp;
}
}
5、希尔排序
希尔排序基于插入排序
1)规则
通过加大插入排序中元素之间的间隔,并在这些有间隔的元素中进行插入排序,
从而使数据项能大跨度的移动。
//TODO
希尔排序通过将比较的全部元素分为几个区间来提升插入排序的性能。
这样可以让一个元素可以一次性地朝最终位置前进一大步。
然后算法再取越来越小的区间间隔进行排序,
算法的最后一步就是普通的插入排序,
但是到了这步,需排序的数据几乎是已排好的了(此时插入排序较快)。
2)代码实现
public void shellSort(int[] array) {
if (array == null || array.length == 0) {
return;
}
int len = array.length;
int inner, outer;
int temp;
int h = 1;//区间间隔
while (h < len / 3) {
//根据数组长度设置合适的区间间隔
h = h * 3 + 1;
}
while (h > 0) {//区间间隔还大于0,继续排序
//以下跟插入排序的实现差不多了
for (outer = h; outer < len; outer++) {
//temp 保存要插入的元素
temp = array[outer];
inner = outer;
/* temp是要插入的元素,
* 那么根据插入排序的规则,temp元素的 h个间隔前的元素是 有序部分的最后一个元素
* 所以,要插入的 temp 元素,与 h个间隔前的元素比较大小,前面元素如果比temp大,则把它挪到temp当前的位置
* 然后 索引减少 h个间隔,与更前面h个间隔的元素作比较,..
* 直到找到比temp小的元素,也就确定的temp的位置,在该元素的 h间隔后面
* 然后把temp插入该位置
*/
while (inner > h - 1 && array[inner - h] >= temp) {
array[inner] = array[inner - h];
inner -= h;
}
array[inner] = temp;
}
//减小间隔,这里要确保最后 h的值是1
h = (h - 1) / 3;
}
}
6、二分插入排序
1)概念
二分插入排序是一种在直接插入排序算法上进行改进的排序算法。
其与直接排序算法最大的区别在于查找插入位置时使用的是二分查找的方式,
在速度上有一定提升。
2)原理
首先要定义三个参数,一个低位索引、一个高位索引、一个中间位索引
总共有N个元素,当插入第i个元素 temp 时,
对前面的0~i-1 个元素进行折半,
低位索引指向0,高位索引指向 i-1
中间位索引 = 低位索引+(高位索引-低位索引)/2
temp 跟中间位那个元素比,
如果比中间元素小,那么高位索引指向中间元素左边一个位置,计算新的中间位索引
temp 继续跟中间位元素比大小 …
如果比中间元素大,那么低位索引指向中间元素右边一个位置,计算新的中间位索引
temp 继续跟中间位元素比大小 …
直到 低位索引 > 高位索引,说明无法再折半了。
这时候的低位索引,就是temp元素的正确排序位置。
然后再把低位索引 low 与 待插入元素temp 之间的所有元素(不包括temp元素,temp元素要插入到前面)后移,
再把temp元素放在目标位置上。
3)代码实现
//从小到大
public void binaryInsertSort(int[] array) {
for (int i = 1; i < array.length; i++) {
int temp = array[i];
int low = 0;//低位索引
int high = i - 1;//高位索引
int mid = -1;//中间位索引
while (low <= high) {
//计算中间位索引
mid = low + (high - low) / 2;
if (array[mid] > temp) {
high = mid - 1;
} else {
//待插入元素 <= 中间元素大小时,也插入在后面的位置
low = mid + 1;
}
}
//所有元素右移一位,腾出空间用来插入 temp元素
for(int j = i - 1; j >= low; j--) {
array[j + 1] = array[j];
}
array[low] = temp;
}
}
7、快速排序
1)概念
通过一趟排序,将待排序记录分割成独立的两部分,
其中一部分记录的关键字均比另一部分小,
然后分别对这两部分记录继续进行排序,直到整个序列有序。
2)原理
在数据集之中,选择一个元素作为“基准”(pivot)。
所有小于“基准”的元素,都移到“基准”的左边;
所有大于“基准”的元素,都移到“基准”的右边。
这个操作称为分区 (partition) 操作,分区操作结束后,
基准元素所处的位置就是最终排序后它的位置。
对“基准”左边和右边的两个子集,不断重复第一步和第二步,
直到所有子集只剩下一个元素为止。
3)代码实现
public void quickSort(int[] arr, int head, int tail) {
if (head >= tail || arr == null || arr.length <= 1) {
return;
}
int i = head;//左边起点索引
int j = tail;//右边起点索引
int pivot = arr[(head + tail) / 2];//中间位置索引
while (i <= j) {//左边索引 <= 右边索引
while (arr[i] < pivot) {//左边元素比基准小,不用移动
++i;//左边索引右移,判断左边的下一个元素
}
while (arr[j] > pivot) {//右边元素比基准大,不用移动
--j;//右边索引左移,判断右边的下一个元素
}
//走到这里,说明左边元素比基准大,且右边元素比基准小
if (i < j) {//左边索引 < 右边索引,则左右元素交换位置
int t = arr[i];
arr[i] = arr[j];
arr[j] = t;
//交换完后,左右索引都向中间移动一位
++i;
--j;
} else if (i == j) {//左右索引相同,
//说明是同一个元素,这元素就是基准元素,不用移动
++i;//左边索引右移,本次交换结束
}
}
//继续用二分法对前半部分进行排序
quickSort(arr, head, j);
//继续用二分法对后半部分进行排序
quickSort(arr, i, tail);
}
二分法的时间复杂度是 logN,快速排序执行了多次二分操作,所以是复杂度要乘以 N
快速排序,时间复杂度为
O
(
N
∗
l
o
g
N
)
O(N*logN)
O(N∗logN)
8、基数排序
1)概念
基数排序是按照低位先排序,然后收集;再按照高位排序,然后再收集;
依次类推,直到最高位.
有时候有些属性是有优先级顺序的,先按低优先级排序,再按高优先级排序。
2)规则
十进制的基数是10,二进制的基数是2。
以十进制为例,
-
根据数据个位数的值,将所有数据分为10组。
个位为0的一组、个位为1的一组、…、个位为9的一组然后,对这10组数据重新排列,个位为0的组排再最前、…、个位 为9的组排在最后。
-
对第1步排序后的数据,根据数据十位数的值,将所有数据分为10组。
十位为0的一组、十位为1的一组、…、十位为9的一组然后,对这10组数据重新排列,十位为0的组排再最前、…、十位 为9的组排在最后。
-
接着是百位…,如果没有高位,则认为高位为0,
把所有位的数字都按以上方法排序完成后,整个数组就是从小到达排序了。
3)代码实现
public void radixSort(int[] array) {
int max = array[0];
for (int i = 0; i < array.length; i++) { //找到数组中的最大值
if (array[i] > max) {
max = array[i];
}
}
//最大值位数
int maxLen = 0;
while (max > 0) {
max /= 10;
maxLen++;
}
//用于临时分类存放每次排序后的数据
List<ArrayList<Integer>> buckets = new ArrayList<ArrayList<Integer>>();
//每位可能的数字为0~9,所以设置10个桶
for (int i = 0; i < 10; i++) {
buckets.add(new ArrayList<Integer>()); //桶由ArrayList<Integer>构成
}
//先按个位分类和排序,逐渐按更高位进行分类和排序
for (int i = 0; i < maxLen; i++) {
//遍历所有数组元素,将元素分配到对应的桶中
for (int j = 0; j < array.length; j++) {
/* 取出该元素对应第i+1位上的数字
* 比如258,
* 取出个位上的数字,258 % 10^1 = 8,8 / 10^0 = 8
* 取出十位数的数字,258 % 10^2 = 58,58 / 10^1 = 5
* Math.pow(10, i):表示 10的i 次幂
*/
int key = array[j] % (int) Math.pow(10, i + 1) / (int) Math.pow(10, i);
//将该元素放入关键字为key的桶中
buckets.get(key).add(array[j]);
}
//分配完之后,将桶中的元素依次复制回数组
int counter = 0; //元素计数器
for (int j = 0; j < 10; j++) {
ArrayList<Integer> bucket = buckets.get(j); //关键字为j的桶
while (bucket.size() > 0) {//把桶中元素,全部拷贝回数组
/* 因为桶ArrayList 放进 buckets中的时候索引就是关键字key
* 所以 buckets 中,桶是有序的
*/
array[counter++] = bucket.remove(0); //将桶中的第一个元素复制到数组,并移除
}
}
}
}
9、堆排序
1)概念
堆排序是选择排序的一种,可以利用数组的特点快速定位指定索引的元素。
堆排序是将数据看成是完全二叉树、根据完全二叉树的特性来进行排序的一种算法
堆分为大根堆和小根堆,是完全二叉树。
最大堆:最大堆中的最大元素值出现在根结点(堆顶)
堆中每个父节点的元素值都大于等于其孩子结点(如果存在)
最小堆:
最小堆中的最小元素值出现在根结点(堆顶)
堆中每个父节点的元素值都小于等于其孩子结点(如果存在)
二叉树的每个结点至多只有二棵子树(不存在度大于 2 的结点),
二叉树的子树有左右之分,次序不能颠倒。
二叉树的第 i 层至多有 2i - 1 个结点;深度为 k 的二叉树至多有 2k - 1 个结点;
-
树和二叉树的三个主要差别:
树的结点个数至少为 1, 而二叉树的结点个数可以为 0
树中结点的最大度数没有限制,而二叉树结点的最大度数为 2
树的结点无左、右之分,而二叉树的结点有左、右之分
二叉树又分为完全二叉树(complete binary tree)和满二叉树(full binary tree)完全二叉树:除了最后一层之外的其他每一层都被完全填充,并且所有结点都保持向左对齐。
满二叉树:除了叶子结点之外的每一个结点都有两个孩子,每一层(当然包含最后一层)都被完全填充。
完满二叉树:除了叶子结点之外的每一个结点都有两个孩子结点。
堆(二叉堆)可以视为一棵完全的二叉树,这使得堆可以利用数组来表示
-
二叉堆(完全二叉树),最后一个非叶子节点索引: a r r a y . l e n g t h 2 − 1 \frac{array.length}{2} - 1 2array.length−1
-
二叉堆(完全二叉树),currentIdx 节点的 左子节点索引:int leftChildIdx = currentIdx * 2 + 1;
(普通的一般的二叉树通常用链表作为基本容器表示),每一个结点对应数组中的一个元素。
2)原理
https://www.cnblogs.com/Java3y/p/8639937.html
https://www.cnblogs.com/skywang12345/p/3602162.html
-
把数组构造成堆,然后堆排序,构造成最大堆
-
根据最大堆的特征,根节点是最大元素。
根节点元素与末节点元素交换位置,末尾元素就变成了最大值了。 -
然后把末尾元素之外的剩余元素为一个新堆,继续按最大堆排序,
排序完后,根节点与新堆的末节点交换位置,
新堆中最大元素移到了新堆的末端,也就是最大元素的前面。
如此往复… -
最终,整个数组都是有序的了。
3)代码实现
/**
* 堆排序的主要入口方法,共两步。
*/
public void heapSort(int[] array) {
/*
* beginIndex = 第一个非叶子节点。
* 从第一个非叶子节点开始,跟它的叶子比较交换,
* 完成之后,索引偏移到上一个非叶子节点,跟叶子节点比较交换
* 如此往复...
* 最终会构造成一个最大堆。
*/
int len = array.length;
int beginIndex = (len / 2) - 1;//第一个非叶子节点
//此循环结束后,就第一次把整个堆,构造成最大堆了
for (int i = beginIndex; i >= 0; i--) {
maxHeapify(array, i, len-1);
}
/*
* 排序,将最大的节点放在堆尾,然后从根节点重新调整
*
* 每次都是移出最顶层的根节点A[0],与最尾部节点位置调换,同时遍历长度 - 1。
* 然后从新整理被换到根节点的末尾元素,使其符合堆的特性。
* 直至未排序的堆长度为 0。
*/
for (int i = len - 1; i > 0; i--) {
swap(array, 0, i);//把最大堆的根节点,挪到数组末端,该元素就已经排序完成了
/* 从第一个元素(堆的根节点)开始,把[0, n-1] 构造成最大堆
* 根节点移到末尾后,最大值必然在根节点的左右两个节点之一
* 每次构造完后,在下一次循环时,触发上面的 swap()方法,会把最大值交换到末尾
*/
maxHeapify(array, 0, i - 1);
}
}
/**
* 调整索引为 currentIdx 处的数据,使其符合最大堆的特性
*
* @param currentIdx 需要堆化处理的数据的索引
* @param len 未排序的堆(数组)最后一个元素的【索引】
*/
private void maxHeapify(int[] array, int currentIdx, int len) {
//左子节点的计算规则,是堆的特征所具有的固定规则
int leftChildIdx = currentIdx * 2 + 1; // 左子节点索引
int rightChildIdx = leftChildIdx + 1; // 右子节点索引
int maxChildIdx = leftChildIdx; // 子节点值最大索引,默认左子节点
if (leftChildIdx > len) {
return;// 左子节点索引超出计算范围,直接返回
}
// 先判断左右子节点,哪个较大
if (rightChildIdx <= len && array[rightChildIdx] > array[leftChildIdx]) {
maxChildIdx = rightChildIdx;//如果右子节点比左节子点大,最大子节点索引指向右子节点
}
if (array[maxChildIdx] > array[currentIdx]) { //最大子节点比当前节点大
swap(array, maxChildIdx, currentIdx); //当前节点与最大子节点交换位置
/* 当前节点换到最大子节点位置之后,该子节点可能不再符合最大堆特性
* 所以这里要把交换后的子节点重新构造成最大堆
*/
maxHeapify(array, maxChildIdx, len);
}
}
private void swap(int[] array, int i, int j) {
int temp = array[i];
array[i] = array[j];
array[j] = temp;
}
堆排序的时间复杂度是 O ( N l o g N ) O(NlogN) O(NlogN)
10、归并排序
1)概念
归并排序Merge Sort
归并排序是建立在归并操作上的一种有效的排序算法,该算法是【采用分治法】的典型应用。
它指的是将两个已经排序的序列合并成一个序列的操作。
归并排序算法依赖归并操作。
归并排序有多路归并排序、两路归并排序 , 可用于内排序,也可以用于外排序。
这里仅对内排序的两路归并方法进行讨论。
2)原理
- 把 n 个记录看成 n 个长度为 1 的有序子表
- 进行两两归并使记录关键字有序,得到 n/2 个长度为 2 的有序子表
- 重复第 2 步直到所有记录归并成一个长度为 n 的有序表为止。
3)代码实现
public static void mergeSort(int[] array) {
sort(array, 0, array.length - 1);
}
public static void sort(int[] array, int leftIdx, int rightIdx) {
if (leftIdx == rightIdx) {
return;
}
//获取中间位置索引
int midIdx = leftIdx + ((rightIdx - leftIdx) / 2);
//前半部分排序
sort(array, leftIdx, midIdx);
//后半部分排序
sort(array, midIdx + 1, rightIdx);
//合并数据
merge(array, leftIdx, midIdx, rightIdx);
}
public static void merge(int[] array, int leftIdx, int midIdx, int rightIdx) {
//用于前后两部分比较大小后,存放有序的合并后的数据的数组
int[] temp = new int[rightIdx - leftIdx + 1];
int i = 0;
int leftStart = leftIdx;
int rightStart = midIdx + 1;
/* 比较左右两部分的元素,哪个小,把那个元素填入temp中
* 左边部分的右边界是midIdx,右边部分的右边界是rightIdx
*/
while (leftStart <= midIdx && rightStart <= rightIdx) {
//比较左右两部分的元素大小,小的放前面
if (array[leftStart] < array[rightStart]) {
temp[i] = array[leftStart];
i++;
leftStart++;
} else {
temp[i] = array[rightStart];
i++;
rightStart++;
}
}
/* 上面的循环退出后,左右两组数据,只有一边还有数据,
* 把剩余的元素依次填入到temp中
* 以下两个while只有一个会执行
*/
while (leftStart <= midIdx) {
temp[i] = array[leftStart];
i++;
leftStart++;
}
while (rightStart <= rightIdx) {
temp[i] = array[rightStart];
i++;
rightStart++;
}
// 把最终的排序的结果复制给原数组
for (i = 0; i < temp.length; i++) {
array[leftIdx + i] = temp[i];
}
}
推荐阅读:
《Java 数据结构和算法 第二版》
刷题---->LeetCode