携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第1天,点击查看活动详情
排序算法经过长时间演变,已很成熟,也有很多方案,每种排序算法都有其优点与缺点,需要在不同场合结合算法特性选择使用。
排序算法大致分为两大类:
- 内排序
- 外排序
内排序指的是在排序过程中所有数据、记录都存在内存中,在内存中完成排序操作。外排序指的是如果待排序数据量庞大,在排序过程中需要使用外部存储设备,需要外部存储设备协助排序来减轻内存压力,存在内外存数据交互。内排序是外排序的基础。
这里列的都是内排序。
内排序又分为:
- 插入排序
- 选择排序
- 交换排序
- 归并排序
- 基数排序
插入排序
插入排序按实现方式不同分为:
- 直接插入排序
- 二分插入排序
- 希尔排序
直接插入排序
每次选中待排序元素,在已有序集合从后向前依次比较,找到合适位置将其插入。直到所有记录完成排序。
图示:
java实现
public class DirectInsertSort {
/**
* @param size 数组大小
* @param lowLimit 下限
* @param highLimit 上限
* @return int[]
*/
public static int[] createRandomColl(int size, int lowLimit, int highLimit) {
Assert.check(size > 0, "数组长度需要大于0");
final int[] coll = new int[size];
for (int i = 0; i < size; i++) {
coll[i] = lowLimit + (int) (Math.random() * (highLimit - lowLimit));
}
return coll;
}
/**
* 直接插入排序
* 关键在于:在已有序集合中从后向前找到待排序元素合适的插入下标
*
* @param source 源集合
*/
public static void directInsertSort(int[] source) {
for (int i = 0; i < source.length; i++) {
//待排序元素
int temp = source[i];
int j;
//在已有序集合中找到合适位置
for (j = i - 1; j >= 0; j--) {
//大于temp的元素后移
if (temp < source[j]) {
source[j + 1] = source[j];
} else {
//找到需要插入的位置j+1,跳出循环
break;
}
}
source[j + 1] = temp;
}
}
public static void main(String[] args) {
final int[] source = createRandomColl(20, -10, 15);
System.out.println("源集合:");
System.out.println(CollectionUtils.arrayToList(source));
System.out.println();
directInsertSort(source);
System.out.println("直接插入排序后");
System.out.println(CollectionUtils.arrayToList(source));
}
}
结果:
二分法插入排序
选中待排序元素temp,在已有序集合中存在三个指针left、mid、right,每次循环temp和mid对应元素比较,循环条件为left<=right,当temp<source[mid]时 right = mid -1,否则left = mid+1。
图示:
java实现
public class DichotomyInsertSort {
public static void dichotomyInsertSort(int[] source) {
for (int i = 0; i < source.length; i++) {
//待排序元素
int temp = source[i];
//每次都从0开始比较
int left = 0;
//【0-(i-1)】元素是有序的
int right = i - 1;
//mid需要动态修改,这里给默认值
int mid = 0;
//left所在下标则为需要插入下标
while (left <= right) {
mid = right + left / 2;
if (temp < source[mid]) {
right = mid - 1;
} else {
left = mid + 1;
}
}
//将left及其之后元素后移
for (int j = i - 1; j >= left; j--) {
source[j + 1] = source[j];
}
//当i==left时说明,待排序元素大于已有序元素集合最大值,则无需排序
if (i != left) {
source[left] = temp;
}
}
}
public static void main(String[] args) {
final int[] source = DirectInsertSort.createRandomColl(20, -10, 15);
System.out.println("源集合:");
System.out.println(CollectionUtils.arrayToList(source));
System.out.println();
dichotomyInsertSort(source);
System.out.println("直接插入排序后");
System.out.println(CollectionUtils.arrayToList(source));
}
}
结果:
希尔排序
希尔排序是将集合按照一定增量进行分组,分成多个子数组,对各个子数组进行直接插入排序,然后依次缩小赠量,直至增量为1进行最后一次排序。
增量的取值范围为:[1,数组长度)
一般首次取 数组长度/2,之后每次循环除以二
如次可以保证最后一次的增量为1。
java实现
实现关键
- 增量的起始量一般为数组长度一半,之后依次除二
- 在此增量上对数组进行分组,分组后进行直接插入排序
比直接插入排序好在哪里
直接插入排序,是一个双循环排序,对于已有序的集合排序友好,但是对于杂乱无章的集合不友好。且直接插入排序在移动元素时步长为1,需要移动多次。而希尔排序移动数组步长为step(增量),移动元素次数变少了,通过多次分组后得到了一个差不多有序的集合,再进行最后一次直接插入排序。
public class XiErInsertSort {
/**
* 希尔排序是对直接插入排序的一种优化
* 直接插入排序的缺点:每次循环只插入一个值,对于有序集合友好,对于杂乱集合,需要频繁移动元素
* 希尔排序优化思路:将源集合以某个增量分组,对各分组内的元素进行直接插入排序(由于增量的存在移动元素次数不再频繁)
* <p>
* 增量选择:一般为集合长度一半,依次除二,直到为1。增量的选择不互为倍数
*
* @param source 源集合
*/
public static void xiErInsertSort(int[] source) {
//增量
int step = 0;
int temp = 0;
for (step = source.length / 2; step > 0; step /= 2) {
//step为分组后的待排序元素,对其进行直接插入排序
for (int j = step; j < source.length; j++) {
temp = source[j];
int i = j - step;
while (i >= 0 && temp < source[i]) {
source[i + step] = source[i];
i -= step;
}
source[i + step] = temp;
}
}
}
public static void main(String[] args) {
final int[] randomColl = DirectInsertSort.createRandomColl(17, 10, 20);
System.out.println("希尔排序前");
System.out.println(CollectionUtils.arrayToList(randomColl));
xiErInsertSort(randomColl);
System.out.println("希尔排序后");
System.out.println(CollectionUtils.arrayToList(randomColl));
}
}
选择排序
每趟从待排序记录中找出最小的元素,放到已排序记录中的最后面,直到排序完成。
大致分为
- 直接选择排序
- 堆排序
直接选择排序
在待排序记录中假设首个元素为最小元素min,待排序存在比min小的元素,则该元素和min元素互换位置,直到排序完成
图示:
java实现
public class DirectSelectInsert {
public static void directSelectInsert(int[] source) {
//待排序记录最小值
int min = 0;
//最小值下标
int minIndex = 0;
for (int i = 0; i < source.length - 1; i++) {
min = source[i];
minIndex = i;
for (int j = i; j < source.length; j++) {
if (source[j] < min){
min = source[j];
minIndex = j;
}
}
//交换
source[minIndex] = source[i];
source[i] = min;
}
}
public static void main(String[] args) {
final int[] randomColl = DirectInsertSort.createRandomColl(20, 10, 20);
System.out.println("直接选择排序前");
System.out.println(CollectionUtils.arrayToList(randomColl));
directSelectInsert(randomColl);
System.out.println("直接选择排序后");
System.out.println(CollectionUtils.arrayToList(randomColl));
}
}
堆排序
堆我们一般指的是二叉堆,及一颗完全二叉树,一般以数组的形式存储。
分类
-
大顶堆
-
及任意父元素的值大于其任意子元素,此刻堆顶为序列最大值
-
-
小顶堆
-
反之为小顶堆
-
性质
- 是一颗完全二叉树
- 有序性,任意父节点值大于(小于)其子节点
数据结构
大顶堆小顶堆图示:
存储结构:
堆一般以数组作为其存储结构,任意节点n,其两个子节点为2n+1和2n+2,如果是大顶堆则父节点大于两个子节点,如果是小顶堆,父节点小于两个子节点。
PriorityQueue
priorityQueue是java提供的使用堆实现的有序队列,可以借助参考
PriorityQueue默认升序排序,即默认小顶堆,不过可以修改Comparter来改变其为大顶堆。
public class HeapSort {
@Test
public void testPriorityQueue() {
final int[] randomColl = DirectInsertSort.createRandomColl(20, 10, 30);
System.out.println("源集合");
System.out.println(CollectionUtils.arrayToList(randomColl));
System.out.println("小顶堆");
final PriorityQueue<Integer> descHeap = new PriorityQueue<>((Collection<? extends Integer>) CollectionUtils.arrayToList(randomColl));
descHeap.forEach(element -> {
System.out.printf(element + ",");
});
System.out.println();
System.out.println("大顶堆");
final PriorityQueue<Integer> ascHeap = new PriorityQueue<>((ele1, ele2) -> ele1 > ele2 ? -1 : 1);
for (int ele : randomColl) {
ascHeap.add(ele);
}
ascHeap.forEach(element -> {
System.out.printf(element + ",");
});
}
}
简单阅读PriorityQueue源码可以发现,实现堆排序,存在以下关键步骤。
- 如何构建一个堆
- 在推出顶堆的时候如何保证,剩余元素可以调整为一个新堆。踢出堆顶元素,剩余元素重新建堆
如何构建一个堆(这里以小顶堆实现)
关键在于堆的性质
①父节点为n那么两个子节点为2n+1和2n+2,那么只需要关注下标为【0,half-1】的元素与其子节点关系即可,如果父节点小于其两个子节点则无需调整,否则选出较小子节点作为新的父节点。因为构成二叉堆之后 【half,size-1】中的元素都为【0,half-1】中某个节点子节点。
②一定从half-1开始调整,这样可以保证下标为0的节点为堆顶
代码实现:
class HeapSortTest {
static int[] heap;
/**
* @param k 为父节点
* @param x 父节点对应元素
*/
void siftDownComparable(int k, int x,int[] source) {
int size = source.length;
//一半
int half = size >>> 1;
while (k < half) {
//当前为左子节点下标,后续更新为较小子节点下标
int child = (k << 1) + 1;
//c当前为左子节点,后续为左右子节点较小值
int c = source[child];
int right = child + 1;
//右子节点小于左子节点,更新child和c
if (right < size && c > source[right]) {
c = source[child = right];
}
//父节点小于较小子节点,跳出循环
if (x <= c) {
break;
}
//更新父节点
source[k] = c;
//记录child,因为child对应元素被移动,需要检查其子节点是否合理
k = child;
}
source[k] = x;
}
/**
* 生成小顶堆
* @param source
*/
private void heapify(int[] source) {
for (int i = (source.length >>> 1) - 1; i >= 0; i--){
siftDownComparable(i, source[i],source);
}
}
public static void main(String[] args) {
final HeapSortTest heapSortTest = new HeapSortTest();
heap = DirectInsertSort.createRandomColl(10, 10, 90);
System.out.println("原数组");
System.out.println(CollectionUtils.arrayToList(heap));
heapSortTest.heapify(heap);
System.out.println("生成一个小顶堆");
System.out.println(CollectionUtils.arrayToList(heap));
}
}
如何堆排序:
循环建堆,踢出堆顶元素即可,当然这不是一个好的解决方案,因为我大量申请内存空间
System.out.println("堆排序后");
for (int i = 0; i < heap.length - 1; i++) {
//待生成堆集合
final int[] tempColl = Arrays.copyOfRange(heap, i, heap.length - 1);
heapSortTest.createHeap(tempColl);
heap[i] =tempColl[0];
}
System.out.println(CollectionUtils.arrayToList(heap));