给你一个整数数组 nums,请你将该数组升序排列。---->咱们碰到这种题目时先要搞清楚数据的特点
,有的时候可能还会给具体的业务场景,在确定采用的算法之后再编码,不要一上来就手撕,闷头就是写。
- 首先我自己封装了一些常用的工具方法、工具类,在排序算法中都有应用,当然以后会不断扩充的
/**
* Copyright (c) 2013-Now http://AIminminAI.com All rights reserved.
*/
package UtilsByHu;
/**
*
* @author HHB
* @version 2022年4月11日
*/
public class ArrayUtils {
/**
* 专门用于打印数组的方法(函数)
* @param arr
* @author HHB
*/
public void printArr(int[] arr){
for (int i = 0; i < arr.length; i++) {
System.out.print("第" + i + "个数组元素是" + arr[i] + " ");
}
}
public void printArr(char[] arr){
for (int i = 0; i < arr.length; i++) {
System.out.print("第" + i + "个数组元素是" + arr[i] + " ");
}
}
/**
* 用于交换数组arr中两个元素a和b的工具方法
* @param arr
* @param a
* @param b
* @return
* @author HHB
*/
public void swapArray(int[] arr, int a, int b){
int temp = arr[a];
arr[a] = arr[b];
arr[b] = temp;
}
}
/**
* Copyright (c) 2013-Now http://AIminminAI.com All rights reserved.
*/
package UtilsByHu;
import java.util.Iterator;
import java.util.List;
/**
*
* @author HHB
* @version 2022年4月15日
*/
public class CollectionUtils {
/**
* 专门迭代遍历并打印出字符串的
* @param list
* @author HHB
*/
public void printCollectionOfString(List<String> list){
Iterator<String> iterator = list.iterator();
while (iterator.hasNext()) {
String string2 = (String) iterator.next();
System.out.print(string2 + " ");
}
}
public void printCollectionOfListString(List<List<String>> list){
Iterator<List<String>> iterator = list.iterator();
while (iterator.hasNext()) {
List<String> string2 = iterator.next();
printCollectionOfString(string2);
}
}
/**
* 专门迭代打印List<List<Integer>>
* @param list
* @author HHB
*/
public void printCollectionOfIntegerDouble(List<List<Integer>> list){
Iterator<List<Integer>> iterator = list.iterator();
while (iterator.hasNext()) {
List<Integer> num = iterator.next();
System.out.print(num + " ");
}
}
/**
* 专门用于迭代打印List<Integer>
* @param list
* @author HHB
*/
public void printCollectionOfInteger(List<Integer> list){
Iterator<Integer> iterator = list.iterator();
while (iterator.hasNext()) {
int num = iterator.next();
System.out.print(num + " ");
}
}
}
- 另外,瞅瞅:维基百科的排序算法
- 选择排序(时间复杂度:O(N^2),这里N是数组的长度;空间复杂度:O(1),使用到常数个临时变量。)。
如果在交换成本较高的排序任务中,就可以使用选择排序
内层负责找(i, nums.length - 1)中最小
:内层for循环用来找到从i开始到数组最后一个元素这个区间中的最小值
,外层负责交换,并每次给i增1
:然后外层循环负责将当前i位置的元素和对应内层循环找到的元素进行交换
/**
* Copyright (c) 2013-Now http://AIminminAI.com All rights reserved.
*/
package SortingAlgorithm;
import UtilsByHu.ArrayUtils;
/**
*
* 快速排序:就是搭建一个舞台,也就是将第一个元素带上舞台,然后让第一个元素和第二个一直到最后一个分别比较一下,然后找出最小值及其索引,然后将这个最小值和第一个元素相互交换。
* 然后针对除第一个元素外剩余的元素们,将舞台搭在第二个元素处,在用第二个与后面每一个轮流比较找出这些元素中最小的,找出最小的与第二个元素交换,
* 然后针对除第二个元素外剩余的元素们......
* 再到第三个....一直到最后一个
*
* (官方概念:每一轮选取未排定的部分中最小的部分交换到未排定部分的最开头,经过若干个步骤,就能排定整个数组。即:先选出最小的,再选出第2小的,以此类推。)
*
* @author HHB
* @version 2022年4月9日
*/
public class SelectionSort {
/**
* 第一個版本,算是先找出整個數組中的最小元素所在位置,并將這個索引值賦值給minValue
* 然後將最小值與數組的第一個元素交換
*
* @param args
* @author HHB
*/
// public static void main(String[] args) {
// int[] arr = new int[] { 2, 5, 4, 3, 7, 6, 1, 9, 8 };
// for (int i = 0; i < arr.length; i++) {
// System.out.print("第" + i + "个数组元素是" + arr[i] + " ");
// }
//
// //搭建一个舞台,放每轮最小的值,最開始假設0這個位置是最小值所在位置
// int minValue = 0;
// int i = 0;
// for (i = 1; i < arr.length - 1; i++) {
// if (arr[i] < arr[minValue]) {
// minValue = i;
// System.out.println(minValue);
// }
// }
// int temp = arr[minValue];
// arr[minValue] = arr[0];
// arr[0] = temp;
// System.out.println("===============");
// for (int j = 0; j < arr.length; j++) {
// System.out.print("第" + j + "个数组元素是" + arr[j] + " ");
// }
// }
/**
* 第二个版本,大体实现选择排序功能,然后在第三阶段中对某些可复用的功能进行封装
* @param args
* @author HHB
*/
// public static void main(String[] args) {
// int[] arr = new int[] { 2, 5, 4, 3, 7, 6, 1, 9, 8, 23, 85, 56, 22, 11, 30 };
// for (int i = 0; i < arr.length; i++) {
// System.out.print("第" + i + "个数组元素是" + arr[i] + " ");
// }
//
//
// for (int k = 0; k < arr.length; k++) {
// //搭建一个舞台,放每轮最小的值,最開始假設0這個位置是最小值所在位置
// int minValue = k;
// for (int i = k + 1; i < arr.length; i++) {
// if (arr[i] < arr[minValue]) {
// minValue = i;
System.out.println(minValue);
// }
// }
// int temp = arr[minValue];
// arr[minValue] = arr[k];
// arr[k] = temp;
// }
//
// System.out.println("===============");
// for (int j = 0; j < arr.length; j++) {
// System.out.print("第" + j + "个数组元素是" + arr[j] + " ");
// }
// }
/**
* 第三个阶段,对某些可复用的功能进行封装,成为方法
* @param args
* @author HHB
*/
public static void main(String[] args) {
int[] arr = new int[] { 2, 5, 4, 3, 7, 6, 1, 9, 8, 23, 85, 56, 22, 11, 30 };
new ArrayUtils().printArr(arr);
selectionSort(arr);
System.out.println("===============");
new ArrayUtils().printArr(arr);
}
//每次记下最小元素的索引,这样就避免了每次两两交换那种,减少了交换次数
public static void selectionSort(int[] arr){
//搭建一个舞台,放每轮最小的值,最開始假設0這個位置是最小值所在位置
//k就是代表每轮最小元素要交换到的目标索引
for (int k = 0; k < arr.length - 1; k++) {
// 选择区间 [i, len - 1] 里最小的元素的索引,交换到下标 i
int minValue = k;//minValue代表每轮中最小元素的索引
for (int i = k + 1; i < arr.length; i++) {
if (arr[i] < arr[minValue]) {
minValue = i;
// System.out.println(minValue);
}
}
new ArrayUtils().swapArray(arr, minValue, k);
}
}
}
/**
* 第四个阶段,比如,人家给你出道题,来给你形参输入一个整数数组,来,排个序,那肯定你要给人家返回一个int[]数组,你看你现在返回的是啥。
* @param args
* @author HHB
*/
public static void main(String[] args) {
int[] arr = new int[] { 2, 5, 4, 3, 7, 6, 1, 9, 8, 23, 85, 56, 22, 11, 30 };
new ArrayUtils().printArr(arr);
selectionSort(arr);
System.out.println("===============");
new ArrayUtils().printArr(arr);
}
/**
* 内层for循环用来找到从i开始到数组最后一个元素这个区间中的最小值,
* 然后外层循环负责将当前i位置的元素和对应内层循环找到的元素进行交换
*/
public static int[] selectionSort(int[] arr){
//循环不变量:[0, k) 有序,且该区间里所有元素就是最终排定的样子
for (int k = 0; k < arr.length - 1; k++) {
int minValue = k;
// 选择区间 [i, len - 1] 里最小的元素的索引,交换到下标i
for (int i = k + 1; i < arr.length; i++) {
if (arr[i] < arr[minValue]) {
minValue = i;
// System.out.println(minValue);
}
}
new ArrayUtils().swapArray(arr, minValue, k);
}
return arr;
}
}
//这个选择排序算法中涵盖的思想:
//算法思想 1:贪心算法:每一次决策只看当前,当前最优,则全局最优。注意:这种思想不是任何时候都适用。
//算法思想 2:减治思想:外层循环每一次都能排定一个元素,问题的规模逐渐减少,直到全部解决,即「大而化小,小而化了」。运用「减治思想」很典型的算法就是大名鼎鼎的「二分查找」。
但是人都为啥不提倡这个选择排序,因为他…
- 堆排序
/**
* Copyright (c) 2013-Now http://AIminminAI.com All rights reserved.
*/
package SortingAlgorithm;
/**
*
* 堆排序是选择排序的优化,选择排序需要在未排定的部分里通过「打擂台」的方式选出最大的元素(复杂度 O(N)),而「堆排序」就把未排定的部分构建成一个「堆」,这样就能以 O(logN)的方式选出最大元素;
*
*「优先队列」是一种特殊的队列,按照优先级顺序出队,从这一点上说,与「普通队列」无差别。「优先队列」可以用数组实现,也可以用有序数组实现,但只要是线性结构,复杂度就会高,因此,「树」结构就有优势,「优先队列」的最好实现就是「堆」。
*
* 时间复杂度:O(NlogN),这里 NNN 是数组的长度;
* 空间复杂度:O(1)。
*
* @author HHB
* @version 2022年6月6日
*/
public class HeapifySort {
public int[] sortArray(int[] nums) {
int len = nums.length;
// 将数组整理成堆
heapify(nums);
// 循环不变量:区间 [0, i] 堆有序
for (int i = len - 1; i >= 1; ) {
// 把堆顶元素(当前最大)交换到数组末尾
swap(nums, 0, i);
// 逐步减少堆有序的部分
i--;
// 下标 0 位置下沉操作,使得区间 [0, i] 堆有序
siftDown(nums, 0, i);
}
return nums;
}
/**
* 将数组整理成堆(堆有序)
*
* @param nums
*/
private void heapify(int[] nums) {
int len = nums.length;
// 只需要从 i = (len - 1) / 2 这个位置开始逐层下移
for (int i = (len - 1) / 2; i >= 0; i--) {
siftDown(nums, i, len - 1);
}
}
/**
* @param nums
* @param k 当前下沉元素的下标
* @param end [0, end] 是 nums 的有效部分
*/
private void siftDown(int[] nums, int k, int end) {
while (2 * k + 1 <= end) {
int j = 2 * k + 1;
if (j + 1 <= end && nums[j + 1] > nums[j]) {
j++;
}
if (nums[j] > nums[k]) {
swap(nums, j, k);
} else {
break;
}
k = j;
}
}
private void swap(int[] nums, int index1, int index2) {
int temp = nums[index1];
nums[index1] = nums[index2];
nums[index2] = temp;
}
}
- 冒泡排序
/**
* Copyright (c) 2013-Now http://AIminminAI.com All rights reserved.
*/
package SortingAlgorithm;
import UtilsByHu.ArrayUtils;
/**
*
* 冒泡排序:从数组第一个元素开始,让第一个元素和第二个元素比较大的移到后面,然后第二个和第三个比较....倒数第二个和倒数第一个比较,最后就把最大的元素移到了最后一格
* 接着,从从数组第一个元素开始,让第一个元素和第二个元素比较大的移到后面,然后第二个和第三个比较....倒数第三个和倒数第二个比较,最后就把第二大元素移到了倒数第二格
* ...
* 最后就把倒数第一大元素放在了第一格
* 为啥要叫冒泡,可以把要排序的数组竖起来,两个一比较两个一比较,那是不是把大的元素嘟嘟嘟滴冒到了数组最后面的格子,倒过来不就是嘟嘟嘟向上冒嘛
*
* @author HHB
* @version 2022年4月13日
*/
public class BubbleSorting {
public static void main(String[] args) {
int[] arr = new int[]{9, 6, 2, 23, 3, 46, 18, 7, 4, 8, 1, 5};
new ArrayUtils().printArr(arr);
bubbleSort(arr);
System.out.println("=====");
new ArrayUtils().printArr(arr);
}
public static void bubbleSort(int[] arr){
/**
* 这两层for循环的终止条件的书写形式都可以
*/
// for (int k = arr.length; k > 0; k--) {
// System.out.println("=====");
// System.out.println(k);
// 但是为了规范一点,还是.length - 1好一点
// for (int i = 0; i < k; i++) {
// if(arr[i] > arr[i + 1]){
// new ArrayUtils().swapArray(arr, i, i + 1);
// }
// }
// }
for (int k = arr.length - 1; k > 0; k--) {
for (int i = 0; i < k - 1; i++) {
if(arr[i] > arr[i + 1]){
new ArrayUtils().swapArray(arr, i, i + 1);
}
}
}
}
}
/**
*下面是优化的几种思路
咱们上面那种是外层递减,所以使得比较次数也是递减的,也算是潜在的优化吧,当然咱们下面这种写法外层递增写法,也是可以的
*/
for(int i = 0; i < arr.length - 1; i++){
//j < arr.length - 1 - i,也可以在外层递增的情况下来减少比较次数
for(int j = 0; j < arr.length - 1 - i; j++){
if(arr[i] > arr[i + 1]){
new ArrayUtils().swapArray(arr, i, i + 1);
}
}
}
/**
*更进一步的,减少冒泡次数,还可以
*/
for(int i = 0; i < arr.length - 1; i++){
//设置一个标志量,表示是否发生了改变。如果后面人家已经有序了,那咱们就没必要在比较一遍了
boolean swapped = false;
//j < arr.length - 1 - i,也可以在外层递增的情况下来减少比较次数
for(int j = 0; j < arr.length - 1 - i; j++){
if(arr[i] > arr[i + 1]){
new ArrayUtils().swapArray(arr, i, i + 1);
//发生交换时,变更标志量
swapped = true;
}
}
if(!swapped){
break;
}
}
/**
*再者,还可以记录下最后一次交换时i的索引,从而保证后面有序时就不比较后面了
*/
- 插入排序:(
时间复杂度:O(N^2),这里N是数组的长度;空间复杂度:O(1),使用到常数个临时变量
。)从第一个位置开始向后面慢慢扫,扫到小的在后面,把这个小的一步一步换到前面合适他的位置(咱们上帝视角看来,不就是,把后面的小的东西,拿出来,插到前面这个小东西该有的位置嘛),所以叫插入排序- 插入排序:
稳定排序
,在接近有序的情况下,表现优异 - 在数组几乎有序的前提下,插入排序的时间复杂度可以达到 O(N);
- 由于插入排序在几乎有序的数组上表现良好,特别地,在短数组上的表现也很好。因为短数组的特点是:每个元素离它最终排定的位置都不会太远。为此,在小区间内执行排序任务的时候,可以转向使用插入排序。
- 插入排序:
/**
* Copyright (c) 2013-Now http://AIminminAI.com All rights reserved.
*/
package SortingAlgorithm;
import UtilsByHu.ArrayUtils;
/**
* 插入排序:将后面的大小不合适的一个元素通过从最后一个一个比较把他换到前面的合适大小的位置
* 就跟打扑克牌一样,如果咱们拿到一张牌要插到前面两张牌中,相当于把比这张牌大的所有后面元素都要向后移动
* @author HHB
* @version 2022年4月22日
*/
public class InsertionSorting {
public static void insertionSort(int[] arr){
/**
* 从第一个位置(上的元素)开始先向后遍历,如果碰到前大后小的元素,就把这个小的插到大的前面(为什么要叫插呢,因为并不是移动一步),比如1532,那这个2是不是要插到35的前面,相当于要交换两次,所以交换两次了那就不能叫普通的交换了,要交插入
*/
for (int i = 1; i < arr.length; i++) {
for (int j = i; j > 0; j--) {
if (arr[j] < arr[j - 1]) {
new UtilsByHu.ArrayUtils().swapArray(arr, j, j - 1);
}
}
}
}
public static void main(String[] args) {
int[] arr = new int[]{9, 6, 2, 23, 3, 46, 18, 7, 4, 8, 1, 5};
new ArrayUtils().printArr(arr);
insertionSort(arr);
System.out.println("=====");
new ArrayUtils().printArr(arr);
}
}
/**
* 插入排序:将后面的大小不合适的一个元素通过从最后一个一个比较把他换到前面的合适大小的位置
* 就跟打扑克牌一样,如果咱们拿到一张牌要插到前面两张牌中,相当于把比这张牌大的所有后面元素都要向后移动
*
* 第二个版本,同样的,题目中,人家就要一个排好序的数组,那咱们是不是得给人家返回int[]数组呀,那你看你第一个版本,就不是返回的数组呀,所以咱们得有第二个版本
*
* @author HHB
* @version 2022年4月22日
*/
public class InsertionSorting {
public static void insertionSort(int[] arr){
/**
* 从第一个位置(上的元素)开始先向后遍历,如果碰到前大后小的元素,就把这个小的插到大的前面(为什么要叫插呢,因为并不是移动一步),比如1532,那这个2是不是要插到35的前面,相当于要交换两次,所以交换两次了那就不能叫普通的交换了,要交插入
*/
//循环不变量:将nums[i]插入到区间[0, i)使之成为有序数组
for (int i = 1; i < arr.length; i++) {
//之前元素逐个后移,留出空位
for (int j = i; j > 0; j--) {
if (arr[j] < arr[j - 1]) {
new UtilsByHu.ArrayUtils().swapArray(arr, j, j - 1);
}
}
/**
*里面这个for循环,也可以这样写,这样写更能体现出插入排序的做法:「将一个数字插入一个有序的数组」这一步,可以不使用逐步交换,使用先赋值给「临时变量」,然后「适当的元素」后移,空出一个位置,最后把「临时变量」赋值给这个空位的策略
*「插入排序」可以提前终止内层循环(体现在 nums[j - 1] > temp 不满足时),在数组「几乎有序」的前提下,「插入排序」的时间复杂度可以达到 O(N)O(N)O(N);
*
* // 先暂存这个元素,然后之前元素逐个后移,留出空位
*int temp = nums[i];
*int j = i;
*// 注意边界j > 0
*//此时在注意边界的前提下,就说明如果nums[j - 1] > temp = nums[i]=nums[j],说明数组此时前大后小,那不得把小的放到前面大的放到后面,那就整呗
*while (j > 0 && nums[j - 1] > temp) {
* nums[j] = nums[j - 1];//把小的赋值给大的位置,还不就是相当于把小的换到前面去了
* j--;//指针步进嘛,没啥说的,你不步进,一直原地踏步走,你能排个啥序
*}
*nums[j] = temp;//你把人家大的覆盖了,这就完了吗?肯定不行呀,得找回场子呀,再把大的找回来
*}
*return nums;
*/
}
}
public static int[] main(String[] args) {
int[] arr = new int[]{9, 6, 2, 23, 3, 46, 18, 7, 4, 8, 1, 5};
new ArrayUtils().printArr(arr);
insertionSort(arr);
System.out.println("=====");
new ArrayUtils().printArr(arr);
}
}
- 归并排序
- 数组归并和链表归并的联系:
/** * Copyright (c) 2013-Now http://AIminminAI.com All rights reserved. */ package SortingAlgorithm; /** * * 归并排序 借助额外空间,合并两个有序数组,得到更长的有序数组 * * 「归并排序」比「快速排序」好的一点是,它借助了额外空间,可以实现「稳定排序」,Java 里对于「对象数组」的排序任务,就是使用归并排序(的升级版 TimSort * 归并排序思路,类似题型:数组中的逆序对、计算右侧小于当前元素的个数 * * 算法思想:分而治之(分治思想)。分而治之思想的形象理解是曹冲称象、MapReduce,在一定情况下可以并行化。 * * 时间复杂度:O(NlogN),这里N是数组的长度; * 空间复杂度:O(N),辅助数组与输入数组规模相当 * * @author HHB * @version 2022年6月6日 */ public class MergeSort { /** * 列表大小等于或小于该大小,将优先于 mergeSort使用插入排序。也就是小区间使用插入排序 */ private static final int INSERTION_SORT_THRESHOLD = 7; /** * * @param nums 对数组 nums的子区间 [left, right] 进行归并排序 * @param left * @param right * @param temp 用于合并两个有序数组的辅助数组,全局使用一份,避免多次创建和销毁.优化 3:全程使用一份临时数组进行「合并两个有序数组」的操作,避免创建临时数组和销毁的消耗,避免计算下标偏移量。 * @author HHB */ private void mergeSort(int[] nums, int left, int right, int[] temp){ // 小区间使用插入排序.优化 1:在「小区间」里转向使用「插入排序」,Java 源码里面也有类似这种操作,「小区间」的长度是个超参数,需要测试决定,这里参考了 JDK 源码; if(right - left <= INSERTION_SORT_THRESHOLD){ //当然,也可以用上面咱们封装好的插入排序,先new出对象再调用呗 insertionSort(nums, left, right); return; } // Java 里有更优的写法,在 left 和 right 都是大整数时,即使溢出,结论依然正确 // int mid = (left + right) >>> 1; int mid = left + (right - left) / 2; mergeSort(nums, left, mid, temp); mergeSort(nums, mid + 1, right, temp); //如果数组的这个子区间本身有序,无需合并.优化 2: 在「两个数组」本身就是有序的情况下,无需合并 if (nums[mid] <= nums[mid + 1]) { return; } mergeOfTwoSortedArray(nums, left, mid, right, temp); } /** * 对数组 arr 的子区间 [left, right] 使用插入排序 * * @param arr 给定数组 * @param left 左边界,能取到 * @param right 右边界,能取到 */ private void insertionSort(int[] arr, int left, int right) { for(int i = insStart + 1; i < insEnd + 1; i++){ for(int j = i; j > insStart; j--){ if(nums[j] < nums[j - 1]){ swap(nums,j, j - 1); } } } } public int[] sortArray(int[] nums) { int[] temp = new int[nums.length]; mergeSort(nums, 0, nums.length - 1, temp); return nums; } /** * 合并两个有序数组:先把值复制到临时数组,再合并回去 * * @param nums * @param left * @param mid [left, mid] 有序,[mid + 1, right] 有序 * @param right * @param temp 全局使用的临时数组 */ private void mergeOfTwoSortedArray(int[] nums, int left, int mid, int right, int[] temp) { System.arraycopy(nums, left, temp, left, right + 1 - left); int i = left; int j = mid + 1; for (int k = left; k <= right; k++) { if (i == mid + 1) { nums[k] = temp[j]; j++; } else if (j == right + 1) { nums[k] = temp[i]; i++; } else if (temp[i] <= temp[j]) { // 注意写成 < 就丢失了稳定性(有了=后相同元素原来靠前的排序以后依然靠前) nums[k] = temp[i]; i++; } else { // temp[i] > temp[j] nums[k] = temp[j]; j++; } } } }
- 数组归并和链表归并的联系:
- 快速排序【
时间复杂度:O(NlogN),这里 NN 是数组的长度;空间复杂度:O(logN),这里占用的空间主要来自递归函数的栈空间
】
- 单边循环快排:
- 第一步:选择最右边元素为基准点元素【当然选择最左边也是可以的,我感觉就是一千个人一千个哈姆雷特,交换和定位方式不同罢了】
- 第二步:定义两个变量或者叫指针,lessThan和greatThan。
- 一开始lessThan和greatThan两个元素都在最左边,然后lessThan指针向右移动负责找比基准点小的元素,每当碰到比基准点大的元素lessThan指针无视这个元素继续向右走,每当碰到比基准点小的元素,lessThan所指元素与greatThan所指元素互换【刚开始人家greatThan还在起点,不过人家greatThan一会就会慢慢移动的嘛,别急】
- 而greatThan是,当交换过程发生一次之后greatThan就向右移动一次,也就是greatThan++
- 一开始lessThan和greatThan两个元素都在最左边,然后lessThan指针向右移动负责找比基准点小的元素,每当碰到比基准点大的元素lessThan指针无视这个元素继续向右走,每当碰到比基准点小的元素,lessThan所指元素与greatThan所指元素互换【刚开始人家greatThan还在起点,不过人家greatThan一会就会慢慢移动的嘛,别急】
- 第三步:最后位于最右侧的基准点与greatThan交换,
greatThan即为分区位置
,这一步其实就是把咱们最开始选择的基准点元素不是在右边嘛,咱们给他移到中间,然后以这个元素为分区点分开原数组,然后对分开后的多个小数组继续递归调用上面的移啊移个换呀换操作【当小数组里面元素小于等于1时就不用再分啦】
- 最后,换完之后呈现的结果就是
pivot所指左边的元素都是比pivot所指的基准点元素小的元素,pivot所指右边的元素都是比pivot所指的基准点元素大的元素
- 最后,换完之后呈现的结果就是
- 以换过来的pivot所在位置为分割位置,将原数组分割为多个小数组,然后分割,分割完成之后递归进行下一轮的循环
- 最简易的一版,当然也很费时间
- 最简易的一版,当然也很费时间
/** * Copyright (c) 2013-Now http://AIminminAI.com All rights reserved. */ package SortingAlgorithm; import java.util.Random; //单边快速排序 /** * * 基本思路:快速排序每一次都排定一个元素(这个元素呆在了它最终应该呆的位置),然后递归地去排它左边的部分和右边的部分,依次进行下去,直到数组有序 * * 算法思想:分而治之(分治思想),与「归并排序」不同,「快速排序」在「分」这件事情上不想「归并排序」无脑地一分为二,而是采用了 partition 的方法,因此就没有「合」的过程。 * * 实现细节(注意事项):(针对特殊测试用例:顺序数组或者逆序数组)一定要随机化选择切分元素(pivot),否则在输入数组是有序数组或者是逆序数组的时候,快速排序会变得非常慢(等同于冒泡排序或者「选择排序」); * * 类似题型:数组中的第 K 个最大元素;颜色分类。 * * 时间复杂度:O(NlogN),这里 N是数组的长度 * 空间复杂度:O(logN),这里占用的空间主要来自递归函数的栈空间。 * * @author HHB * @version 2022年6月6日 */ class Solution { //列表大小等于或小于该大小,将优先于 quickSort 使用插入排序 private static final int INSERTION_SORT_THRESHOLD = 7; public int[] sortArray(int[] nums) { quickSort(nums, 0, nums.length - 1); return nums; } public void quickSort(int[] nums, int sortStart, int sortEnd){ //>=刚好包含了,=表示分的数组最后就剩一个元素了,就没必要再分了;另外就是如果返回的pivotIndex正好等于sortStart,那么相当于sortStart大于pivotIndex - 1=分区额右边界了,也应该停止了 if(sortStart >= sortEnd){ return; } // 小区间使用插入排序 if (sortEnd - sortStart <= INSERTION_SORT_THRESHOLD) { insertionSort(nums, sortStart, sortEnd); return; } int pivotIndex = partition(nums, sortStart, sortEnd); quickSort(nums, sortStart, pivotIndex - 1); quickSort(nums, pivotIndex + 1, sortEnd); } private static final Random RANDOM = new Random(); //sortStart和sortEnd分别代表,你这是要对数组的从哪里到哪里开始快速排序呀 //我是对sortStart和sortEnd这一段进行快排呀 public int partition(int[] nums, int sortStart, int sortEnd){ int randomIndex = RANDOM.nextInt(sortEnd - sortStart + 1) + sortStart; swap(nums, sortStart, randomIndex); //选择最右边元素为基准元素 int pivot = nums[sortEnd]; int greatThan = sortStart; for(int lessThan = sortStart; lessThan < sortEnd; lessThan++){ //lessThan指针向右移动,每当碰到比基准点小的元素,lessThan 所指元素与greatThan所指元素互换 if(nums[lessThan] < pivot){ //lessThan一旦找到比基准点pivot小的则与greatThan进行交换 swap(nums, lessThan , greatThan); greatThan++; } } swap(nums, sortEnd, greatThan); //返回一轮快排完了之后,基准点元素应该换到greatThan所在位置,因为greatThan是当lessThan每找到一个比基准点元素小的元素并和自己greatThan交换之后时他才向右步进一步,所以此时greatThan左边的都是比pivot小的而greatThan右边都是比pivot大的,所以你greatThan赶紧乖乖的把位置腾出来给pivot,好让人家知道从哪分出下一轮的小数组们呀 return greatThan; } /** * 对数组 nums 的子区间 [left, right] 使用插入排序 * * @param nums 给定数组 * @param left 左边界,能取到 * @param right 右边界,能取到 */ private void insertionSort(int[] nums, int left, int right) { for (int i = left + 1; i < right + 1; i++) { for (int j = i; j > left; j--) { if (nums[j] < nums[j - 1]) { swap(nums, j, j - 1); } } } } /**private void insertionSort(int[] nums, int left, int right) { for (int i = left + 1; i <= right; i++) { int temp = nums[i]; int j = i; while (j > left && nums[j - 1] > temp) { nums[j] = nums[j - 1]; j--; } nums[j] = temp; } }**/ public void swap(int[] nums, int index1, int index2){ int temp = nums[index1]; nums[index1] = nums[index2]; nums[index2] = temp; } }
- 优化点:
- 上面的两次交换,其实咱们就可以做几次判断,然后少交换几次,就可以提高程序效率了
- 第一步:选择最右边元素为基准点元素【当然选择最左边也是可以的,我感觉就是一千个人一千个哈姆雷特,交换和定位方式不同罢了】
- 双边循环快排
- 第一步:选择最左边元素为基准点元素
- 第二步:定义两个变量分别是lef和right,left从最左边开始向右走找比基准点大的,right从右边向左走找比基准点小的。谁先找到谁先等等,等着对方也找到了,双方就进行交换。
- 第三步:等到left和right指针相遇,这俩任意一个和pivot交换即可
class Solution { private static final int INSERTION_SORT_THRESHOLD = 7; private static final Random RANDOM = new Random(); public int[] sortArray(int[] nums) { quickSort(nums, 0, nums.length - 1); return nums; } public void quickSort(int[] nums, int sortStart, int sortEnd){ //>=刚好包含了,=表示分的数组最后就剩一个元素了,就没必要再分了;另外就是如果返回的pivotIndex正好等于sortStart,那么相当于sortStart大于pivotIndex - 1=分区额右边界了,也应该停止了 if(sortStart >= sortEnd){ return; } // 小区间使用插入排序 if (sortEnd - sortStart <= INSERTION_SORT_THRESHOLD) { insertionSort(nums, sortStart, sortEnd); return; } int pivotIndex = partition(nums, sortStart, sortEnd); quickSort(nums, sortStart, pivotIndex - 1); quickSort(nums, pivotIndex + 1, sortEnd); } /** * 对数组 nums 的子区间 [left, right] 使用插入排序 * * @param nums 给定数组 * @param left 左边界,能取到 * @param right 右边界,能取到 */ private void insertionSort(int[] nums, int left, int right) { for (int i = left + 1; i <= right; i++) { int temp = nums[i]; int j = i; while (j > left && nums[j - 1] > temp) { nums[j] = nums[j - 1]; j--; } nums[j] = temp; } } //sortStart和sortEnd分别代表,你这是要对数组的从哪里到哪里开始快速排序呀 //我是对sortStart和sortEnd这一段进行快排呀 public int partition(int[] nums, int sortStart, int sortEnd){ int randomIndex = left + RANDOM.nextInt(right - left + 1); swap(nums, randomIndex, left); //选择最左边元素为基准元素 int pivot = nums[sortStart]; int left = sortStart; int right = sortEnd; while(left < right){ //定义两个变量分别是lef和right,left从最左边开始向右走找比基准点大的,right从右边向左走找比基准点小的。谁先找到谁先等等,等着对方也找到了,双方就进行交换。 //这俩也得是先right动再left动,不这样到最后left==right时就有可能把大元素交换到基准点那里, while(nums[right] > pivot){ right--; } while(nums[left] < pivot){ left++; } swap(nums, left, right); } //第三步交换基准点到中间 swap(nums, left, pivot);//swap(nums, right, pivot)也行 return left;//return right } public void swap(int[] nums, int index1, int index2){ int temp = nums[index1]; nums[index1] = nums[index2]; nums[index2] = temp; } }
- 优化点:
- 加个内层left < right
- <=是防止pivot和left相等,然后比完之后改交换了,就有可能把基准点交换走了,就有问题了。有了=,咱们就可以执行到left++,把基准点就跳过去了
- 加个内层left < right
- 单边循环快排:
- 希尔排序
/**
* Copyright (c) 2013-Now http://AIminminAI.com All rights reserved.
*/
package SortingAlgorithm;
import UtilsByHu.ArrayUtils;
/**
* 希尔排序:选定一个间隔,从索引0开始,每次经过一个间隔找出一个数,然后等走到数组后面没办法再往后面走时,对选好的这一组数用选择排序进行排序
* 然后从第一个间隔中也就是索引1开始,继续按照相同的间隔区一组数进行插入排序
* ......
* 最后排完之后,大致的(不一定是非常准确的),小的数都跑到了前面并且大的数都跑到了后面
*
* 然后缩小间隔,再重复上面过程,排完序
* ...
* 最后直到间隔为1,排序一遍之后,排序结束
* @author HHB
* @version 2022年5月9日
*
*
* 那希尔排序可以说作为插入排序的增强版,强在哪里或者说快在哪里?
* 快在,比如说,咱们现在倒数第二个数字是1,如果用选择排序的话,咱们只能倒数第二个和倒数第三个比,大的放后面小的放前面,然后倒数第三个和倒数第四个比,大的放后面小的放前面
* ......
* 最后第二个与第一个比,最后才有可能把一放到了最开始合适的位置,时间复杂度为O(N)
* 那假如说我用希尔排序的话,如果我选的间隔正好使得第一组数中涵盖倒数第二个数字也就是1,那么是不是1就很快被排到了前面合适的位置,相比较而言就比插入排序快一点
*
*
* 希尔排序不稳定,跳着排
* 希尔排序的时间复杂度O(N^1.3).
* 没有用到额外的空间所以空间复杂度为O(1)
*/
public class ShellSorting {
public static void main(String[] args) {
int[] arr = new int[]{9, 6, 2, 23, 3, 46, 18, 7, 4, 8, 1, 5};
new ArrayUtils().printArr(arr);
insertionSort(arr);
System.out.println();
new ArrayUtils().printArr(arr);
}
/**
* 第一个版本,咱随便指定一个间隔,让他先排序出大约的顺序,这个顺序肯定不是最终的准确顺序
* 比如:
* 第0个数组元素是9 第1个数组元素是6 第2个数组元素是2 第3个数组元素是23 第4个数组元素是3 第5个数组元素是46 第6个数组元素是18 第7个数组元素是7 第8个数组元素是4 第9个数组元素是8 第10个数组元素是1 第11个数组元素是5
* 第0个数组元素是3 第1个数组元素是6 第2个数组元素是1 第3个数组元素是5 第4个数组元素是4 第5个数组元素是8 第6个数组元素是2 第7个数组元素是7 第8个数组元素是9 第9个数组元素是46 第10个数组元素是18 第11个数组元素是23
* @param arr
* @author HHB
*/
// private static void insertionSort(int[] arr) {
// /**
// * 指定一个间隔
// */
// int gap = 4;
//
// for (int i = gap; i < arr.length; i++) {
// for (int j = i; j > gap - 1; j-=gap) {
// if (arr[j] < arr[j - gap]) {
// new UtilsByHu.ArrayUtils().swapArray(arr, j, j - gap);
// }
// }
// }
// }
/**
* 第二个版本,咱们每次让间隔缩小一倍
* @param arr
* @author HHB
*/
// private static void insertionSort(int[] arr) {
// for (int gap = 4; gap > 0; gap /= 2) {
// for (int i = gap; i < arr.length; i++) {
// for (int j = i; j > gap - 1; j-=gap) {
// if (arr[j] < arr[j - gap]) {
// new UtilsByHu.ArrayUtils().swapArray(arr, j, j - gap);
// }
// }
// }
// }
// }
/**
* 第三个版本,咱们不能每次都让间隔gap为4吧。咱们让间隔为gap = arr.length / 2
* @param arr
* @author HHB
*/
// private static void insertionSort(int[] arr) {
// /**
// * 使用位运算,除以2相当于右移一位
// * for (int gap = arr.length >> 1; gap > 0; gap /= 2) {
// */
// for (int gap = arr.length / 2; gap > 0; gap /= 2) {
// for (int i = gap; i < arr.length; i++) {
// for (int j = i; j > gap - 1; j-=gap) {
// if (arr[j] < arr[j - gap]) {
// new UtilsByHu.ArrayUtils().swapArray(arr, j, j - gap);
// }
// }
// }
// }
// }
/**
* 第四个版本,gap = 3 * gap + 1,比取中点效果更好
* @param arr
* @author HHB
*/
private static void insertionSort(int[] arr) {
/**
* gap = 3 * gap + 1,比取中点效果更好,但是为了使得变量不重复,前面gap先用h代替
*/
int h = 1;
while(h <= arr.length / 3){
h = h * 3 + 1;
}
for (int gap = h; gap > 0; gap = (gap - 1) / 3) {
for (int i = gap; i < arr.length; i++) {
for (int j = i; j > gap - 1; j-=gap) {
if (arr[j] < arr[j - gap]) {
new UtilsByHu.ArrayUtils().swapArray(arr, j, j - gap);
}
}
}
}
}
}
未完待续
巨人的肩膀:
bilibili好多讲算法的老师
https://leetcode.cn/problems/sort-an-array/solution/fu-xi-ji-chu-pai-xu-suan-fa-java-by-liweiwei1419/
labuladong老师,推荐一下他的公众号,干货多多
https://blog.csdn.net/qq_35344198/article/details/106857042