这是《数据结构与算法分析》中排序的Java实现,还包括我自己一些总结(比如PriorityQueue),非递归快排和基数排序还没有完成,先贴代码(基本解释都在注释里),之后再整理。
package sort;
import java.util.*;
/**
* @author mazheng
* @title: Sort
* @projectName workDemo
* @description: TODO
* @date 2021/12/1310:03
*/
public class Sort {
private static int CUTFF = 10;
public static void main(String[] args) {
Random random = new Random();
int[] nums = random.ints(100, 0, 100).toArray();
printArray(nums);
//LinkedList<Integer> list = new LinkedList();
//for (int i : nums) {
// list.add(i);
//}
// 冒泡
// new Sort().bubbleSort(nums);
// 选择
// new Sort().selectSort(nums);
// 插入排序
// new Sort().insertSort(nums);
// 希尔排序
// nums = shellSort(nums);
// 堆排
// nums = heapSort(nums);
// 归并排序
// nums = shellSort(nums);
// 简单快排,参数为List
// quickSortParaList(list);
// 递归快排
nums = quickSortRecursion(nums);
// 非递归快排(还没有完成)
//nums = quickSort(nums);
// 桶排序(一个例子是基数排序,未完成应用不多,在小数量的时候效果较好)
// 打印一下排序后的数组
printArray(nums);
}
/**
* @author mazheng
* @date 2022/1/4 17:14
* @Description TODO 这里数组类型实际上需要定义为可以接受任何可比较类型的,但是为了简单,下面都用int或者Integer类型的数组,实际的泛型可以参考下面的空函数
*/
public static <Anytype extends Comparable<? super Anytype>> void insertSort(Anytype[] a) {
}
/**
* @author mazheng
* @date 2022/1/4 17:15
* @Description 冒泡
* 比较相邻的元素。如果第一个比第二个大,就交换他们两个。
* 对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对。这步做完后,最后的元素会是最大的数。
* 针对所有的元素重复以上的步骤,除了最后一个。
* 持续每次对越来越少的元素重复上面的步骤,直到没有任何一对数字需要比较。
*/
public void bubbleSort(int[] nums) {
// 外循环确定插入的位置
for (int i = 0; i < nums.length; i++) {
//内循环找到最小值
for (int j = nums.length - 2; j >= i; j--) {
if (nums[j] > nums[j + 1]) {
swap(nums, j, j + 1);
}
}
}
System.out.println(Arrays.toString(nums));
}
/**
* @author mazheng
* @date 2022/1/4 17:16
* @Description 选择 选择排序和冒泡排序的区别 : 冒泡排序是立刻交换,选择排序是找到最值之后再交换位置,相当于冒泡的优化
*/
public void selectSort(int[] nums) {
int min;
for (int i = 0; i < nums.length; i++) {
min = i;
for (int j = nums.length - 1; j > i; j--) {
if (nums[j] < nums[min]) {
min = j;
}
}
swap(nums, min, i);
}
System.out.println(Arrays.toString(nums));
}
/**
* @author mazheng
* @date 2022/1/4 17:16
* @Description 插入排序 将数组分为两部分,一部分(如左边,也就是下标小的部分)有序,右边无序,将无序中某个值插入到有序的位置
*/
public static void insertSort(int[] nums) {
int j;
for (int i = 1; i < nums.length; i++) {
int tmp = nums[i];
// 若num[j-1]大于tmp,那么就把它向后移一位,直到遇到比tmp小的位置,将空出来的那个值(此时下表为j,应该为空值)替换为tmp。
for (j = i; j > 0 && nums[j - 1] > tmp; j--) {
nums[j] = nums[j - 1];
}
nums[j] = tmp;
}
//System.out.println(Arrays.toString(nums));
}
/**
* @author mazheng
* @date 2022/1/4 17:16
* @Description 插入排序另一种写法,类比冒泡写的,冒泡到他所在的位置就退出
*/
public void insertSort1(int[] nums) {
//外循环为有序序列下标
for (int i = 0; i < nums.length - 1; i++) {
//内循环为右侧无序序列递进
for (int j = i + 1; j > 0; j--) {
if (nums[j] < nums[j - 1]) {
// 找不到其位置就逐步向左冒泡
swap(nums, j - 1, j);
} else {
// 找到其位置就退出
break;
}
}
}
System.out.println(Arrays.toString(nums));
}
/**
* @author mazheng
* @date 2022/1/4 17:17
* @Description 参数为List的快排(这个是 《 数据结构与算法 》 里面引出标准快排的一个例子 , 名称叫 : 简单的递归排序算法 。
*我感觉这个很符合快排的思想描述 , 只是参数类型有点特殊, 并且产生很多多余list , 所以才不如经典快排效率高)
* 简单快排的思路:
* 1,取一个基准(这里需要单独写一写,具体可以查看《数据结构与算法》),根据基准将排序序列分为大于基准,等于,小于三部分
* 2,对这三部分分别进行快排
* 3,将这三个部分整合起来,合成一个排过序的序列
*/
public static void quickSortParaList(List<Integer> list) {
if (list.size() > 1) {
// 1,基准,一般取下标0或者下标中值,若是排序过的列表,以0为下标效率非常差,所以经常用中值,或者更进一步的三数中值(mid(左,右,中))
Integer base = list.get(list.size() / 2);
// 分割为三部分
List<Integer> smaller = new LinkedList<>();
List<Integer> same = new LinkedList<>();
List<Integer> bigger = new LinkedList<>();
for (Integer i : list) {
if (i > base) {
bigger.add(i);
} else if (i < base) {
smaller.add(i);
} else {
same.add(i);
}
}
//2, 对三部分分别进行快排
quickSortParaList(smaller);
//quickSortParaList(same);
quickSortParaList(bigger);
//3,合并
list.clear();
list.addAll(smaller);
list.addAll(same);
list.addAll(bigger);
}
}
/**
* @author mazheng
* @date 2022/1/4 17:18
* @Description TODO 经典快排入口
* 快排的平均运行时间为O(NlogN),最坏运行时间为O(N^2),但稍许努力这种最坏情况就极难出现
* 经典递归快排
* 快排是事件中一种快速的排序算法,在C++和Java的基本类型排序 中特别有用,这个算法之所以特别快,是因为非常精炼和高度优化的内部循环
* 快排也是应用的分治的策略。
* 经典快排思路:基本同简单快排,但是步骤有所不同
* 在小数组的时候(N<20),插入排序比快排要快
*/
public static int[] quickSortRecursion(int[] a) {
quickSortRecursion(a, 0, a.length - 1);
return a;
}
/**
* @author mazheng
* @date 2022/1/7 15:10
* @Description TODO 经典快排主程序
*/
private static void quickSortRecursion(int[] a, int left, int right) {
// 这里是对应小数组的时候最好用插入排序而不是快排
if (left + CUTFF <= right) {
int pivot = median3(a, left, right);
// 开始分割
int i = left, j = right - 1;
for (; ; ) {
// 下面两个空循环是为了找到第一个i<j的条件下的可以互相交换的数据,如果没有,就说明枢纽左边的数据都小于pivot,右边的数据都大于pivot
while (a[++i] < pivot) {
}
while (a[--j] > pivot) {
}
if (i < j) {
swap(a, i, j);
} else {
break;
}
}
//重置枢纽元
swap(a, i, right - 1);
// 分治
quickSort(a, left, i - 1);
quickSortRecursion(a, i + 1, right);
} else {
// 小数组进行插入排序,这样可以加速程序的运行
insertSort(a);
}
}
/**
* @author mazheng
* @date 2022/1/7 15:32
* @Description TODO 枢纽元的三数中值分割法
*/
private static int median3(int[] a, int left, int right) {
// 下面三个if语句是选择枢纽元的三数中值分割法
int center = (left + right) / 2;
if (a[center] < a[left]) {
swap(a, left, center);
}
if (a[right] < a[left]) {
swap(a, right, left);
}
if (a[right] < a[center]) {
swap(a, right, center);
}
// 将枢纽元放到数组a的right-1的位置
swap(a, center, right - 1);
return a[right - 1];
}
// 非递归快排
public static int[] quickSort(int[] a, int left, int right) {
return a;
}
/**
* @author mazheng
* @date 2022/1/5 16:28
* @Description TODO 优先级队列可以用于以O(NlogN)时间的排序,基于该思想的排序叫堆排序,其中,建立N个元素的二叉堆需要O(N),
* 进行N次取小操作(deleteMin,思想和代码可以参考《数据结构与算法分析》优先级队列那一章)花费O(logN),所以共需要
* O(NlogN)的时间,关于如何避免产生另一个数组的方法也可以参考数据结构与算法分析
*/
public static int[] heapSort(int[] a) {
//构建堆
for (int i = a.length / 2 - 1; i >= 0; i--) {
percDown(a, i, a.length);
}
for (int i = a.length - 1; i > 0; i--) {
//deleteMax,将最小的值(也就是下标为0的值,小根堆的根节点)和最后一个下标的值交换位置,这样可以节省一个数组的空间
swap(a, 0, i);
// 因为经过交换操作,需要将这个时候根节点的值再次下沉调整
percDown(a, 0, i);
}
return a;
}
/**
* @author mazheng
* @date 2022/1/6 10:15
*
* @param i 从哪个位置开始下滤(percolate down)
* @param n 二叉堆的大小
* @Description TODO 这里可以参考二叉堆的构建,基本上就是一个值下滤的过程
*/
private static void percDown(int[] a, int i, int n) {
int child;
int tmp;
for (tmp = a[i]; leftChild(i) < n; i = child) {
child = leftChild(i);
if (child != n - 1 && a[child] < a[child + 1]) {
child++;
}
if (tmp < a[child]) {
a[i] = a[child];
} else {
break;
}
}
a[i] = tmp;
}
/**
* @author mazheng
* @date 2022/1/6 9:45
* @Description TODO 这里是根据满二叉树特性,求得一个节点左子树下标,若是用数组存储满二叉树,那么根据下标就可以获得左子树节点的值
*/
private static int leftChild(int i) {
return 2 * i + 1;
}
/**
* @author mazheng
* @date 2022/1/5 16:44
* @Description TODO 使用Java自带的优先级队列进行堆排序
*/
public static int[] heapSort1(int[] nums) {
PriorityQueue<Integer> priorityQueue = new PriorityQueue<>();
for (int i : nums) {
priorityQueue.add(i);
}
//再进行一次循环,将优先级队列中的数据放到nums中
int i = 0;
while (!priorityQueue.isEmpty()) {
nums[i++] = priorityQueue.poll();
}
return nums;
}
/**
* @author mazheng
* @date 2022/1/5 14:43
* @Description TODO 希尔排序是第一批冲破二次屏障的排序算法之一,主要步骤如下:
* 1,外循环:获取gap,并依次除以2循环直到1
* 2,内循环: 将数组按gap分组,每组进行插入排序
* <p>
* 希尔排序的最坏情况是O(N^2),使用Hibbard增量可以使希尔排序最坏情况为O(N^(3/2)),所以对于数万条数据情况下,希尔排序仍然是可用的(具体证明查看数据结构与算法分析第三版)
*/
public static int[] shellSort(int[] a) {
int j;
// gap是希尔排序的特色,用来依间隔把数组分成多个组,然后每个组进行插入排序,gap依次除以2操作,到1的时候就不需要再分了,因为1/2 = 0,跳出第一层循环
for (int gap = a.length / 2; gap > 0; gap /= 2) {
for (int i = gap; i < a.length; i++) {
// 这个第二层循环做的事情是,将a[i]取出来放到tmp中,找到a[i]在0-i的下标的本组数据中的正确位置,然后挪到那个位置
int tmp = a[i];
for (j = i; j >= gap && tmp < a[j - gap]; j -= gap) {
// 若tmp < a[j - gap] 就将a[j - gap]向后挪一个gap,就像冒泡一样
a[j] = a[j - gap];
}
a[j] = tmp;
}
}
return a;
}
/**
* @author mazheng
* @date 2022/1/7 11:29
* @Description TODO 归并排序入口
*/
public int[] mergeSort(int[] a) {
int[] temArray = new int[a.length];
mergeSort(a, temArray, 0, a.length - 1);
return temArray;
}
/**
* @param a 原始数组
* @param tmpArray 存放结果
* @param left 本次操作的左边界下标
* @param right 本次操作的右边界下标
* @author mazheng
* @date 2022/1/7 10:12
* @Description TODO 归并排序 处理逻辑
* 归并排序以O(NlogN)最坏事件运行,所使用的比较次数几乎是最优,是递归算法一个好的实例,基本操作是合并两个已经排序的表
* 归并算法也是经典的分治策略:将比较大的问题拆分成结构相似的子问题,然后合并求得最终答案。
*/
public static void mergeSort(int[] a, int[] tmpArray, int left, int right) {
if (left < right) {
int center = (left + right) / 2;
mergeSort(a, tmpArray, center + 1, right);
merge(a, tmpArray, left, center + 1, right);
}
}
/**
* @param leftPos 最左的边界
* @param rightPos 第二部分开始的位置
* @param rightEnd 最右边界
* @author mazheng
* @date 2022/1/7 11:18
* @Description TODO 分治策略中的合并部分
*/
private static void merge(int[] a, int[] tmpArray, int leftPos, int rightPos, int rightEnd) {
int leftEnd = rightPos - 1;
int tmpPos = leftPos;
int numElements = rightEnd - leftPos + 1;
//若 两个子块都有值的情况下
while (leftPos <= leftEnd && rightPos <= rightEnd) {
if (a[leftPos] <= a[rightPos]) {
tmpArray[tmpPos++] = a[leftPos];
} else {
tmpArray[tmpPos++] = a[rightPos++];
}
}
// 经过上面操作后,只有左边子块有数据
while (leftPos <= leftEnd) {
tmpArray[tmpPos++] = a[leftPos++];
}
// 经过上面操作后,只有右边子块有数据
while (rightPos <= rightEnd) {
tmpArray[tmpPos++] = a[rightPos++];
}
//回写回数组a
for (int i = 0; i < numElements; i++, rightEnd--) {
a[rightEnd] = tmpArray[rightEnd];
}
}
// 计数排序
// 桶排序
// 基数排序
// 通用函数》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》
/**
* @author mazheng
* @date 2022/1/4 17:18
* @Description 交换两个值
*/
public static void swap(int[] nums, int i, int j) {
int tmp;
tmp = nums[i];
nums[i] = nums[j];
nums[j] = tmp;
}
/**
* @author mazheng
* @date 2022/1/4 17:19
* @Description TODO 这是一个空函数,原本的设计是利用泛型可以接收任意类型的数据进行比较
*/
public void compare(int[] nums, int i, int j) {
}
/**
* @author mazheng
* @date 2022/1/5 15:06
* @Description TODO 打印数组
*/
public static void printArray(int[] nums) {
System.out.println(Arrays.toString(nums));
}
}