常见排序算法(插入排序&选择排序)

122 阅读6分钟

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第1天,点击查看活动详情

排序算法经过长时间演变,已很成熟,也有很多方案,每种排序算法都有其优点与缺点,需要在不同场合结合算法特性选择使用。

排序算法大致分为两大类:

  • 内排序
  • 外排序

内排序指的是在排序过程中所有数据、记录都存在内存中,在内存中完成排序操作。外排序指的是如果待排序数据量庞大,在排序过程中需要使用外部存储设备,需要外部存储设备协助排序来减轻内存压力,存在内外存数据交互。内排序是外排序的基础。

这里列的都是内排序。

内排序又分为:

  1. 插入排序
  2. 选择排序
  3. 交换排序
  4. 归并排序
  5. 基数排序

插入排序

插入排序按实现方式不同分为:

  • 直接插入排序
  • 二分插入排序
  • 希尔排序

直接插入排序

每次选中待排序元素,在已有序集合从后向前依次比较,找到合适位置将其插入。直到所有记录完成排序。

图示:

image-20220630235422574.png

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));
    }
}

结果: image-20220701004023551.png

二分法插入排序

选中待排序元素temp,在已有序集合中存在三个指针left、mid、right,每次循环temp和mid对应元素比较,循环条件为left<=right,当temp<source[mid]时 right = mid -1,否则left = mid+1。

图示:

image-20220701012030107.png

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));
    }
}

结果:

image-20220701013424481.png

希尔排序

希尔排序是将集合按照一定增量进行分组,分成多个子数组,对各个子数组进行直接插入排序,然后依次缩小赠量,直至增量为1进行最后一次排序。

增量的取值范围为:[1,数组长度)

一般首次取 数组长度/2,之后每次循环除以二

如次可以保证最后一次的增量为1。

image-20220701031349142.png

java实现

实现关键

  • 增量的起始量一般为数组长度一半,之后依次除二
  • 在此增量上对数组进行分组,分组后进行直接插入排序

比直接插入排序好在哪里

直接插入排序,是一个双循环排序,对于已有序的集合排序友好,但是对于杂乱无章的集合不友好。且直接插入排序在移动元素时步长为1,需要移动多次。而希尔排序移动数组步长为step(增量),移动元素次数变少了,通过多次分组后得到了一个差不多有序的集合,再进行最后一次直接插入排序。

image-20220701031349142.png

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));

    }
}

image-20220701125108984.png


选择排序

每趟从待排序记录中找出最小的元素,放到已排序记录中的最后面,直到排序完成。

大致分为

  • 直接选择排序
  • 堆排序

直接选择排序

在待排序记录中假设首个元素为最小元素min,待排序存在比min小的元素,则该元素和min元素互换位置,直到排序完成

图示:

image-20220701131934897.png

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));
    }
}

image-20220701134132928.png

堆排序

堆我们一般指的是二叉堆,及一颗完全二叉树,一般以数组的形式存储。

分类
  • 大顶堆

    • 及任意父元素的值大于其任意子元素,此刻堆顶为序列最大值

  • 小顶堆

    • 反之为小顶堆

性质
  • 是一颗完全二叉树
  • 有序性,任意父节点值大于(小于)其子节点
数据结构

大顶堆小顶堆图示:

image-20220702000507644.png

存储结构:

堆一般以数组作为其存储结构,任意节点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 + ",");
        });
    }
}

image-20220702002501120.png

简单阅读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));
    }
}

image-20220702013301753.png

如何堆排序:

循环建堆,踢出堆顶元素即可,当然这不是一个好的解决方案,因为我大量申请内存空间

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));

image-20220702013311720.png