常见排序算法(交换排序&归并排序&基数排序)

95 阅读6分钟

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

交换排序

即在合适的位置交换元素,循环结束后可以达到集合有序的目的。

大致分为

  • 冒泡排序
  • 快速排序

冒泡排序

同样对一个集合分为有序和无序两部分,冒泡排序会对无序集合自上到下两两相邻的元素依次进行比较和调整,较大元素下沉,较小元素上浮,也就是当相邻两个元素之间的排序规则与预期相悖则交换他们的位置。

图示:

java实现

大致可以分为两种思路:每次循环将最大值下沉和每次循环将最小值上浮。

需要注意的是内循环,循环的次数,减少非必要循环。

public class BubbleSort {
    /**
     * 上浮
     *
     * @param source
     */
    public static void bubbleSortUp(int[] source) {
        int temp = 0;
        for (int i = source.length - 1; i > 0; i--) {
            //减i是因为,每次循环都会将,较小值上浮没必要比较到0
            for (int j = source.length - 1; j > source.length - i; j--) {
                if (source[j] < source[j - 1]) {
                    //交换
                    temp = source[j];
                    source[j] = source[j - 1];
                    source[j - 1] = temp;
                }
            }
        }
    }

    /**
     * 下沉
     * 将最大值下沉
     *
     * @param source
     */
    public static void bubbleSortDown(int[] source) {
        int temp = 0;
        for (int i = 0; i < source.length; i++) {
            //减一是因为,每次循环都会将,较大值下沉没必要比较到0
            for (int j = 0; j < source.length - i - 1; j++) {
                if (source[j] > source[j + 1]) {
                    //交换
                    temp = source[j];
                    source[j] = source[j + 1];
                    source[j + 1] = temp;
                }
            }
        }
    }

    public static void main(String[] args) {
        final int[] randomColl = DirectInsertSort.createRandomColl(20, 10, 100);
        System.out.println("源集合:=》" + CollectionUtils.arrayToList(randomColl));
        bubbleSortUp(randomColl);
        System.out.println("up冒泡排序后:=》" + CollectionUtils.arrayToList(randomColl));
        System.out.println("================");

        final int[] randomColl2 = DirectInsertSort.createRandomColl(20, 10, 3000);
        System.out.println("源集合:=》" + CollectionUtils.arrayToList(randomColl2));
        bubbleSortDown(randomColl2);
        System.out.println("down冒泡排序后:=》" + CollectionUtils.arrayToList(randomColl2));
    }
}

image-20220705231044049.png

快速排序

以一个元素为基准,经过一趟排序,将一个无序数组分为两部分,一部分比基准大,一部分比基准小,此刻基准元素处于正确的位置,再对这两个无序数组进行递归,直到完成排序。

关键算法

  • 将基准元素放置正确位置,并返回下标
伪代码
public class QuickSort {

    public static void quick(int[] source) {
        quickSort(source, 0, source.length - 1);
    }

    /**
     * 快速排序
     *
     * @param source 无序集合
     * @param prev   开始下标
     * @param last   结束下标
     */
    public static void quickSort(int[] source, int prev, int last) {
        //递归,需要结束标志,无此标志形成死循环
        if (prev < last) {
            final int middle = getMiddle(source, prev, last);
            quickSort(source, middle + 1, last);
            quickSort(source, prev, middle - 1);
        }
    }
    public static int getMiddle(int[] source, int prev, int last) {
        return 1;
    }
}

那么重要的就是如何获取基准元素下标的算法了:

public static int getMiddle(int[] source, int prev, int last) {
    //以首个元素为基准
    final int middleElement = source[prev];
    while (prev < last){
        //找到右边比基准小的元素
        while (prev < last & middleElement <= source[last]){
            last--;
        }
      //将其放到左边
        source[prev] = source[last];
        //找到左边比基准大的元素
        while (prev < last & middleElement >= source[prev]){
            prev++;
        }
      //将其放到右边
        source[last] = source[prev];
    }
    source[prev] = middleElement;
    return prev;
}

测试

public static void main(String[] args) {
    final int[] randomColl = DirectInsertSort.createRandomColl(30, 10, 100);
    System.out.println("源集合:=》" + CollectionUtils.arrayToList(randomColl));
    quick(randomColl);
    System.out.println("快排后:=》" + CollectionUtils.arrayToList(randomColl));
}

image-20220705234205092.png

归并排序

归并排序会申请一个辅助数组空间,用于将两个已有序数组组合成一个新的有序数组,同时涉及到递归算法。

图示:

image-20220706004726131.png

同样的写一个伪代码:

public class MergeSort {
    public static void split(int[] source, int prev, int last) {
        //这是一个归并,得有结束归并的条件
        if (prev < last) {
            int mid = (prev + last) / 2;
            //左边
            split(source, prev,mid);
            //右边
            split(source,mid+1,last);
            //合并
            merge(source, prev,mid,last);
        }
    }

    private static void merge(int[] source, int prev, int mid, int last) {
        
    }
}

同样的重点也是这个合并算法:

大致思路:左右两边逐个比较,将较小的填入协助数组,然后将左边剩余和右边剩余依次填入协助数组,最后更新原数组。

private static void merge(int[] source, int prev, int mid, int last) {
    //协助数组长度
    final int tempLength = last - prev + 1;
    //协助数组
    final int[] temp = new int[tempLength];
    //左右两边起始下标
    int i = prev;
    int j = mid + 1;
    //协助数组起始下标
    int k = 0;

    while (i <= mid & j <= last) {
        //i 较小, i后移,且对应元素记录协助数组
        if (source[i] < source[j]) {
            temp[k++] = source[i++];
        } else {
            temp[k++] = source[j++];
        }
    }

    //将左边剩余元素记录进协助数组
    while (i <= mid) {
        temp[k++] = source[i++];
    }
    //将左边剩余元素记录进协助数组
    while (j <= last) {
        temp[k++] = source[j++];
    }

    //跟新原数组
    for (int index = 0; index < tempLength; index++) {
        source[prev++] = temp[index];
    }
}

测试:

public static void main(String[] args) {

    final int[] randomColl = DirectInsertSort.createRandomColl(20, 10, 100);
    System.out.println("原数组:=》" + CollectionUtils.arrayToList(randomColl));
    split(randomColl, 0, randomColl.length - 1);

    System.out.println("归并排序后=》" + CollectionUtils.arrayToList(randomColl));

}

image-20220706014212583.png

基数排序

首先对所有待排序集合元素同一长度,高位不足补0。依次从低位(个位)开始排序,直到最高位排序完成整个集合变为一个有序集合。

思想就是高位决定权较大,所以得靠后。

图示:

image-20220706022728219.png

步骤:

  • 计算最大数存在几位(确定循环次数)
  • 需要一个桶存放各位相同的元素(十个 0 - 9 存在十位)
java实现

获取集合最大数,并得到其最大位数,用于确定循环次数

/**
 * 获取最大基数
 *
 * @param source 数组
 * @return 最大基数
 */
public static int getMaxRadix(int[] source) {
    int radix = 0;
    int max = source[0];
    for (int ele : source) {
        max = Math.max(max, ele);
    }
    do {
        radix++;
    } while ((max /= 10) > 0);
    return radix;
}

进行基数排序,重点在于

  • 元素应该放入哪个桶?使用先取整再取余
  • 一次循环结束需要清空桶
public static void radixSort(int[] source) {

    //获取最大基数
    final int maxRadix = getMaxRadix(source);
    //创建十个桶,这里使用ArrayList
    final List<ArrayList<Integer>> buckets = new ArrayList<>();
    for (int i = 0; i < 10; i++) {
        buckets.add(new ArrayList<>());
    }

    for (int i = 0; i < maxRadix; i++) {
        int index;
        ArrayList<Integer> arrayList;
        //分配 数据放在哪个桶里
        for (int ele : source) {
            //先取整、再取余。 找到对应的桶
            index = ele / (int) Math.pow(10, i) % (int) Math.pow(10, 1);
            arrayList = buckets.get(index);
            arrayList.add(ele);
        }

        int indexX = 0;
        //收集
        for (int j = 0; j < buckets.size(); j++) {
            final ArrayList<Integer> tTemp = buckets.get(j);
            //桶不为空,写会集合
            while (!tTemp.isEmpty()) {
                source[indexX++] = tTemp.get(0);
                //移出桶
                tTemp.remove(0);
            }
        }
    }
}

测试

public static void main(String[] args) {

    final int[] randomColl = DirectInsertSort.createRandomColl(30, 10, 150);
    System.out.println("源集合:=》" + CollectionUtils.arrayToList(randomColl));
    radixSort(randomColl);
    System.out.println("基数排序后:=》" + CollectionUtils.arrayToList(randomColl));
}

image-20220706030944289.png


计数排序

对于一个无序集合,想要确定任意元素的正确位置,我们只需要知道该序列中存在多少个比该元素小的元素,假设有k个,那么该元素的正确下标为k+1。

计数数组会额外申请两个数组空间,空间复杂度较高。

基数排序涉及一个辅助数组和一个计数数组,计数数组用于记录某个元素存在多少比其小的元素个数,辅助数组用于存放有序元素并最后拷贝到原数组中。

假设有一个无序集合存在n个元素,任意元素大小范围为 0 - m,首先通过一次循环,将每个元素出现的次数记录进count[]数组,再通过一次循环将count[n] + count[n-1]得出某个元素之前存在的记录数,那么就通过这个计数数组就可以确认任意元素的正确下标。

java实现

public class CountSort {
    public static void main(String[] args) {
        int[] source = DirectInsertSort.createRandomColl(20, 0, 20);
        System.out.println("源数组:=》"+CollectionUtils.arrayToList(source));
        countSort(source);
        System.out.println("计数排序后:=》"+CollectionUtils.arrayToList(source));
    }
    /**
     * 计算任意数组的最大元素值,用于生成辅助数组
     *
     * @param source
     * @return
     */
    public static int maxElement(int[] source) {
        Assert.isTrue(null != source && source.length > 0, " 数组不可为空");
        int max = source[0];
        for (int ele : source) {
            max = Math.max(ele, max);
        }
        return max;
    }


    public static void countSort(int[] source) {
        //计数数组个数
        final int count = maxElement(source) + 1;
        //初始化计数数组
        int[] counts = new int[count];
        //计算各个元素出现的次数
        for (int element : source) {
            counts[element]++;
        }
        //计算任意元素存在多少比其小的元素个数,下标从1开始
        for (int i = 1; i < counts.length; i++) {
            counts[i] = counts[i] + counts[i - 1];
        }

        //需要一个辅助数组
        final int[] target = new int[source.length];
        //从后往前
        for (int i = source.length - 1; i >= 0; i--) {
            target[counts[source[i]] - 1] = source[i];
            //计数器减一
            counts[source[i]]--;
        }
        System.arraycopy(target, 0, source, 0, source.length);
    }
}

image-20220707133223349.png


桶排序

桶排序和基数排序的思想比较相似。

桶排序是将一个较长的集合拆分为多个较短的集合,每一个短集合称为桶,对桶内元素进行排序(排序算法可自定),最后将集合进行合并。

  • 每个桶有对应编号,桶内元素随编号增长存在有序性

  • 桶的个数由待排序集合的大小关系决定,一般为 max - min 个

    所以存在计算待排序集合的最大最小值的算法

  • 待排序集合元素落于哪个桶,由自定义算法决定,且必须体现桶之间的有序性

​ 一般为((value-min) / (max-min+1.0) * bucketNum),表现为一个随元素大小递增的特性

image-20220707211224212.png

java实现

public class BuckSort {
    /**
     * 计算待排序集合最大值
     *
     * @param source
     * @return max
     */
    public static int collMax(int[] source) {
        int max = source[0];
        for (int element : source) {
            max = Math.max(element, max);
        }
        return max;
    }
    /**
     * 计算待排序集合最小值
     *
     * @param source
     * @return min
     */
    public static int collMin(int[] source) {
        int min = source[0];
        for (int element : source) {
            min = Math.min(element, min);
        }
        return min;
    }
    public static void bucketSort(int[] source) {
        //获取最值
        int max = collMax(source);
        int min = collMin(source);
        //确定桶的个数n
        int bucketNum = max - min;
        //初始化桶
        List<ArrayList<Integer>> buckets = new ArrayList<>(bucketNum);
        for (int i = 0; i < bucketNum; i++) {
            buckets.add(new ArrayList<Integer>());
        }
        //将元素放入桶内
        for (int value : source) {
            //元素对应桶下标
            int index = (int) ((value - min) / (max - min + 1.0) * bucketNum);
            buckets.get(index).add(value);
        }
        //桶内排序
        for (int i = 0; i < buckets.size(); i++) {
            Collections.sort(buckets.get(i));
        }
        //合并
        int j = 0;
        for (ArrayList<Integer> bucket : buckets) {
            for (int value : bucket) {
                source[j++] = value;
            }
        }
    }
    public static void main(String[] args) {
        final int[] randomColl = DirectInsertSort.createRandomColl(20, 5, 35);
        System.out.println("原集合:=>" + CollectionUtils.arrayToList(randomColl));
        bucketSort(randomColl);
        System.out.println("桶排序:=>" + CollectionUtils.arrayToList(randomColl));
    }
}