「Java」- 八大排序(1)

int j = i - gap;

for (; j >= 0; j -= gap) {

if (elem[j] > tmp) {

elem[j + gap] = elem[j];

} else {

break;

}

}

elem[j + gap] = tmp;

}

}

public void shellSort(int[] elem) {

int gap = elem.length;

while (gap > 1) {

gap = gap / 3 + 1; // 希尔排序的增量是不确定的

shell(elem, gap);

}

}

优化:希尔排序快不快主要取决于怎么去取增量序列 , 比如二分法或者knuth序列法 有兴趣的读者可以去了解一下

时间复杂度: O(n ^ (1.3 - 2))

空间复杂度:O(1)

稳定性: 不稳定


堆排序

=======

原理:指利用堆这种数据结构所设计的一种排序算法。堆是一个近似完全二叉树的结构,并同时满足堆积的性质:即子结点的键值或索引总是小于(或者大于)它的父节点。

  1. 大顶堆:每个节点的值都大于或等于其子节点的值,在堆排序算法中用于升序排列

  1. 小顶堆:每个节点的值都小于或等于其子节点的值,在堆排序算法中用于降序排列

由于堆排序涉及到数据结构中堆的知识 如果还没学过堆的读者 可以先去作者的另一篇文章 「Java数据结构」- 堆(优先级队列)") 了解一下, 在排序之间要先将数组元素构建成一个堆**<建大堆还是小堆,取决于排升序还是排降序 , 作者这里讲解的是排升序 , 构建的是大堆>**

堆排序(升序)动图演示

构建完堆之后 , 由于堆的特性 , 在堆顶的元素是数组中最大的元素, 所以将堆顶与数组最后元素进行交换 ,然后再从堆顶进行向下调整将第 N 大的元素调到堆顶 , 所以我们只需要控制向上调整的边界 , 然后重复 交换向上调整 的操作即可编写出堆排序

代码实现

// 向下调整代码

public void adjustDown(int[] elem, int parent, int len) {

int child = 2 * parent + 1;

while (child < len) {

if (child + 1 < len && elem[child] < elem[child + 1]) {

child++;

}

if (elem[parent] < elem[child]) {

swap(elem, child, parent);

parent = child;

child = parent * 2 + 1;

} else {

break;

}

}

}

// 建堆代码

public void createHeap(int[] elem) {

for (int i = (elem.length - 1 - 1) / 2; i >= 0; i–) {

adjustDown(elem, i, elem.length);

}

}

// 堆排序

public void heapSort(int[] elem) {

createHeap(elem);

int len = elem.length - 1;

//从后往前调整

while (len > 0) {

//交换堆顶和堆尾的元素

swap(elem, 0, len);

//重新调整

adjustDown(elem, 0, len);

len–;

}

}

重点:排升序建大堆 , 排降序建小堆

==========================

时间复杂度: O(n * logn)

空间复杂度:O(1)

稳定性: 不稳定


快速排序

========

原理:从待排序区间选择一个数,作为基准值 , 遍历整个待排序区间,将比基准值小的(可以包含相等的)放到基准值的左边,将比基准值大的(可 以包含相等的)放到基准值的右边 , 采用分治思想,对左右两个小区间按照同样的方式处理,直到小区间的长度 == 1,代表已经有序,或者小区间 的长度 == 0,代表没有数据

快速排序挖坑法动图演示

实现快排的方法有多种 <Hoare 法 , 挖坑法 , 前后指针法> 这里作者挑 挖坑法 来给大家进行讲解

挖坑法

给定一组数据

思路

选区间最左边作为基准值 使用变量 key 进行保存 , 给定 low 变量指向这个区间的最左边 且 low 起始位置就是一个**“坑”, 给定 high 变量指向这个区间最最右边 , 由于基准值被变量 key 保存了 , 所以可以认定最左边的数可以进行覆盖 , 也就形成了一个’‘坑’'** , 可以供其它值进行填坑

此时先从 high 位置开始向前找比基准值 key 更小的值 , 如果遇到比 key 值大的元素则继续向前寻找 , 直至找到更小的元素 , 找到时则将该值填入到 low 位置上 , 此时在 high 位置也形成了一个可覆盖的 “坑”

随着 high 位置停止向前移动 , 接下来就使 low 位置开始向后寻找大于 key 值的元素 , 遇到小于 key 值的元素则继续向后走 , 直至找到比 key 值更大的元素则停止移动 , 此时将该 low 位置上的元素填入刚才 high 形成的 “坑”

重复以上两个操作 ,那怎么控制什么时候不继续往下找呢 , 给的结束条件是 : 如果 low 指针和 high 指针相遇了 , 这个时候就可以将基准值 key 放到两指针相遇的位置 , 当前这个 key 值的左边是比它小的数 , 而 key 值的右边就是比它大的数 , 此时的 key 值就处于有序的位置上

注意:如果选基准时 , 选的是最左边的元素为基准 , 那么第一次找元素必须先从右边开始寻找!!!

到这还未结束 , 挖坑法只是辅助某个元素放到有序集合中属于它的正确位置上 , 而快速排序还有一个核心思想 : 分治思想

分治

分治,字面上的解释是“分而治之”,就是把一个复杂的问题分成两个或更多的相同或相似的子问题,再把子问题分成更小的子问题……直到最后子问题可以简单的直接求解,原问题的解即子问题的解的合并

在快排中的分治 , 是将一个大的无序区间分成多个小的无序区间 , 快速排序中通过 挖坑法 计算出基准值位置 , 而无序区间就从基准值位置开始分割 , 基准左边为一个无序区间 , 基准值位置右边为一个无序区间 , 而基准值已经有序不参与分割

随着小区间再进行计算基准值 与 分割 , 直至区间长度为 1 时 停止切割 , 使用变量 start 记录当前区间的最左边的位置 , 使用变量 end 记录当前区间最右边的位置 , 当每次挖坑法执行完之后 , 用 pivot 记录基准位置 , 所以可以使用递归模拟分割区间

递推公式 : quickSort(start , end) = quick(start , pivot - 1) + quick(pivot + 1 , end)

代码实现

private int partition(int[] elem, int low, int high) {

int key = elem[low];//保存基准值,形成一个坑

while (low < high) {

//找比基准值小的值,记录该下标

while (low < high && elem[high] >= key) {

high–;

}

//找到后将左边形成的坑放置比基准小的值,此时右边就新形成了一个坑

elem[low] = elem[high];

//找比基准值大的值,记录该下标

while (low < high && elem[low] <= key) {

low++;

}

//找到后将右边形成的坑放置比基准大的值,此时左边就新形成了一个坑

elem[high] = elem[low];

}

//low 和 high相遇时,一定是个坑,此时将基准放到low下标处

elem[low] = key;

return low;

}

private void quick(int[] elem, int start, int end) {

//递归结束条件

if (start >= end) {

return;

}

int pivot = partition(elem, start, end); // 拿到基准值位置

quick(elem, start, pivot - 1); // 在基准左边形成一个区间

quick(elem, pivot + 1, end); // 在基准右边形成一个区间

}

public void quickSort(int[] elem) {

quick(elem, 0, elem.length - 1);

}

作者这里介绍的是简单版本的快速排序 , 这个快速排序未经过任何优化 , 有兴趣的读者可以去了解一下关于快速排序的优化方法

时间复杂度: O(n * logn)

空间复杂度:O(n * logn)

稳定性: 不稳定


归并排序

========

原理:将已有序的子序列合并,得到完全有序的序列,即先使每个子序列有序,再使子 序列段间有序

归并排序动图演示

分割

归并排序同样采用的是分治思想 , 归并排序是先进行分割 , 将一个大区间分割成多个小区间 , 所有区间以中间值作为基准 , 从每个区间中间值开始切割 , 直至区间长度为 1 时停止分割

对于分割区间的过程 , 我们可以使用递归来模拟 , 我们可以定义一个变量 start 记录当前区间最左边 , 变量 end 记录区间最右边 , mid 变量记录基准位置 由于是通过中间值来进行切割 , 可以得出 左区间 = start ~ mid 而 右区间 = mid+1 ~ end , 所以可以得到递推公式如下

递推公式:mergeSort(start , end) = merge(start , mid)  + merge(mid+1 , end)

合并

当分割区间长度为 1 时就进行合并 , 合并过程中以基准值分割的两个区间中的元素进行比较 , 放入到一个新的数组中

用变量 s1 记录左区间开始的索引位置 low , s1 索引结束位置为 mid , 再用变量 s2 记录右区间的开始的索引位置mid + 1 , s2 索引结束位置为 high ,两个索引在原数组中进行元素比较大小 , 小的元素先放到新数组中 , 随后对应的索引向前走一步 , 当两个指针其中一个索引超出了索引结束位置时退出比较 , 将剩下没超过索引位置的索引 , 从当前位置开始向后所有的元素添加到新的数组中 , 直至超出索引位置。

经过以上操作 , 此时的新数组中的元素是有序的 , 然后将新数组中的元素重新拷贝到原数组中 , 拷贝完成后 , 原数组中 low 至 high 区间中的元素就是有序的。

重复以上合并的操作 , 直至所有区间都合并完成 , 这个时候就排序完成了

代码实现

// 合并操作

// 3

public void merge(int[] elem, int low, int mid, int high) {

//用一个临时数组用来交换顺序

int[] tmp = new int[high - low + 1];

int s1 = low; // 左区间开始位置

int s2 = mid + 1; // 右区间开始位置

int k = 0;//临时数组的索引开始位置

// 比较两个区间中的元素

while (s1 <= mid && s2 <= high) {

if (elem[s1] <= elem[s2]) {

tmp[k++] = elem[s1++];

} else {

tmp[k++] = elem[s2++];

}

}

//循环结束两种情况

//1、左区间还剩数值没有被放到tmp数组上

while (s1 <= mid) {

tmp[k++] = elem[s1++];

}

//2、右区间还剩数值没有被放到tmp数组上

while (s2 <= high) {

tmp[k++] = elem[s2++];

}

//将tmp数组的值放回到原数组中

//[i+low] 就是当前的左区间下标开始位置

for (int i = 0; i < tmp.length; i++) {

elem[i + low] = tmp[i];

}

}

// 切割操作

// 2

public void mergeSortInternal(int[] elem, int start, int end) {

//递归终止条件,拆分到只有一个数的时候停止递归

if (start >= end) {

return;

}

//选出中间下标作为拆分基准

int mid = start + (end - start) / 2;

//递归拆分

mergeSortInternal(elem, start, mid); // 左区间

mergeSortInternal(elem, mid + 1, end);// 右区间

//切割完成后进行合并

merge(elem, start, mid, end);

}

// 归并排序

// 1

public void mergeSort(int[] elem) {

mergeSortInternal(elem, 0, elem.length - 1);

}

时间复杂度: O(n * logn)

空间复杂度:O(n)

稳定性: 稳定


计数排序

========

计数排序是一种牺牲内存空间来换取低时间的排序算法,同时它也是一种不基于比较的算法。这里的不基于比较指的是数组元素之间不存在比较大小的排序算法

计数排序动图演示

计数排序是一种空间换取时间的算法 , 先要在原数组中找到最大的数, 使用 max 记录 , 最小的数使用 min 记录 , 随后需要一个长度为 max - min + 1 的新数组

而新数组是以 原数组的元素与最小值的差值 作为索引值 , 每个索引上都会记录原数组的元素出现了多少次

遍历原数组 ,每个元素都以与最小值的差值在新数组中记录 , 遍历完成之后 , 再遍历新数组 , 将新数组的下标加最小值的和放回到原数组 , 当新数组中下标记录的值为 0 后 , 向后寻找下一个记录大于 0 的下标 , 然后在执行下标加最小值的和放回到原数组操作 , 直至遍历完新数组 , 此时排序就完成了.

代码实现

// 计数排序

public void countingSort(int[] elem) {

int max = elem[0];

int min = elem[0];

for (int value : elem) {

if (value > max)

max = value;

if (value < min) {

min = value;

}

}

// 最大值与最小值的差值+1做为新数组长度

int k = max - min + 1;

int[] counter = new int[k];

for (int i : elem) {

counter[i - min]++; // 原数组的元素 i 与最小值 min 的差值当做新数组的索引 , 且在新数组当前索引上的元素进行自增一次

}

int j = 0; // 原数组的索引开始位置

// 遍历新数组

for (int i = 0; i < counter.length; i++) {

// 查看当前索引记录是否大于 0

while ((counter[i]–) > 0) {

elem[j++] = i + min; // 将当前索引位置加 min 还原原数组的元素放回原数组

}

}

}

计数排序的优点是速度快 , 不过缺点也很明显 , 计数排序只能直接应用于非负整数的排序中 , 如果需要排序的数据含有负数,或者是其他类型的值,那么,还需要在不改变相对大小的情况下映射成非负整数,使整个排序逻辑变得复杂。

最后

由于细节内容实在太多了,为了不影响文章的观赏性,只截出了一部分知识点大致的介绍一下,每个小节点里面都有更细化的内容!

小编准备了一份Java进阶学习路线图(Xmind)以及来年金三银四必备的一份《Java面试必备指南》

最大值与最小值的差值+1做为新数组长度

int k = max - min + 1;

int[] counter = new int[k];

for (int i : elem) {

counter[i - min]++; // 原数组的元素 i 与最小值 min 的差值当做新数组的索引 , 且在新数组当前索引上的元素进行自增一次

}

int j = 0; // 原数组的索引开始位置

// 遍历新数组

for (int i = 0; i < counter.length; i++) {

// 查看当前索引记录是否大于 0

while ((counter[i]–) > 0) {

elem[j++] = i + min; // 将当前索引位置加 min 还原原数组的元素放回原数组

}

}

}

计数排序的优点是速度快 , 不过缺点也很明显 , 计数排序只能直接应用于非负整数的排序中 , 如果需要排序的数据含有负数,或者是其他类型的值,那么,还需要在不改变相对大小的情况下映射成非负整数,使整个排序逻辑变得复杂。

最后

由于细节内容实在太多了,为了不影响文章的观赏性,只截出了一部分知识点大致的介绍一下,每个小节点里面都有更细化的内容!

[外链图片转存中…(img-7QN09SFG-1714490048403)]

小编准备了一份Java进阶学习路线图(Xmind)以及来年金三银四必备的一份《Java面试必备指南》

[外链图片转存中…(img-ANy1Jn3N-1714490048404)]

本文已被CODING开源项目:【一线大厂Java面试题解析+核心总结学习笔记+最新讲解视频+实战项目源码】收录

  • 26
    点赞
  • 29
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值