十大排序算法-Java实现

105 阅读9分钟

Gitee:gitee.com/zhangziyi11…

记待排序的数组为 int[] arr,数组长度为 n。升序排序。

排序算法大致可分两类:基于比较的排序和基于收集的排序。基于比较的排序属于原地排序,时间复杂度无法突破O(nlog(n))O(nlog(n)),基于收集的排序使用额外空间辅助,时间复杂度可以继续突破。

1. 选择排序

算法思想:对前 n-1个位置,选择其后的最小者,将其交换到该位置。

时间复杂度:O(n1+n2+...+1)=O(n2)O(n-1+n-2+...+1)=O(n^2)

稳定性:不是两两相邻元素比较,因此不稳定

public void sort(int[] arr) {
    for (int i = 0; i < arr.length - 1; i++) {
        // 此轮循环中最小值和最小值下标
        int min = arr[i], minIndex = i;
        for (int j = i; j < arr.length; j++) {
            if (arr[j] < min) {
                min = arr[j];
                minIndex = j;
            }
        }
        swap(arr, i, minIndex);
    }
}

2. 冒泡排序

算法思想:对后 n-1 个位置,将最大的数两两比较移动至此,具体做法是:两两比较,大者右移,像一个水中的气泡不断上浮。

时间复杂度:O(n1+n2+...+1)=O(n2)O(n-1+n-2+...+1)=O(n^2)

稳定性:相邻元素两两比较,是稳定的。

public void sort(int[] arr) {
    for (int i = arr.length - 1; i > 0; i--) {
        for (int j = 1; j <= i; j++) {
            if (arr[j - 1] > arr[j]) {
                swap(arr, j, j - 1);
            }
        }
    }
}

3. 插入排序

算法思想:初始情况下,将首个元素视为排好序的序列,从第二个元素开始,不断插入并形成排好序的序列。

时间复杂度:O(1+2+...+n1)=O(n2)O(1+2+...+n-1)=O(n^2)

稳定性:由于是两两比较交换,因此是稳定的。

public void sort(int[] arr) {
    for (int i = 1; i < arr.length; i++) {
        // 将 arr[i] 插入 [0,...,i-1],方法是不断比较并交换
        for (int j = i; j > 0; j--) {
            if (arr[j] < arr[j - 1]) {
                swap(arr, j, j - 1);
            } else {
                break;
            }
        }
    }
}

4. 希尔排序

算法思想:也叫缩小增量排序,是插入排序的变体。选取一个缩小增量 ti 序列,如 [len/2, len/4, ... ,1],对于每个增量 ti,对这 ti 个子序列使用插入排序。最后当 ti=1 时,便是对整个序列排序。

时间复杂度:平均为O(nlog(n))O(nlog(n))

稳定性:在大于 1 的序列进行插入排序时,并非两两相邻比较,非稳定。

public class ShellSorter implements Sorter{
    @Override
    public void sort(int[] arr) {
        for (int t = arr.length / 2; t >= 1; t /= 2) { // 对每个增量
            for (int i = 0; i < t; i++) { // 对每个子序列
                generalInsertSort(i, t, arr);
            }
        }
    }

    private void generalInsertSort(int startIndex, int incr, int[] arr) {
        for (int i = startIndex + incr; i < arr.length; i += incr) {
            for (int j = i; j > startIndex; j -= incr) {
                if (arr[j] < arr[j - incr]) {
                    swap(arr, j, j - incr);
                } else {
                    break;
                }
            }
        }
    }
}

5. 归并排序

算法思想:使用分治思想,将序列分成两个子序列,分别进行归并排序,再使用双指针进行合并。

时间复杂度:O(nlog(n))O(nlog(n))

稳定性:指定优先选取前半序列的元素,因此是稳定的。

@Override
public void sort(int[] arr) {
    mergeSort(arr);
}

private void mergeSort(int[] arr) {
    if (arr.length == 1) {
        return;
    }
    // 分割点
    int splitPoint = arr.length / 2;
    int[] firstHalf = Arrays.copyOfRange(arr, 0, splitPoint);// 前半序列
    int[] secondHalf = Arrays.copyOfRange(arr, splitPoint, arr.length); // 后半序列
    mergeSort(firstHalf);
    mergeSort(secondHalf);
    merge(arr, firstHalf, secondHalf);
}

private void merge(int[] arr, int[] firstHalf, int[] secondHalf) {
    // 双指针,分别指向前半序列和后半序列
    int p1 = 0, p2 = 0;
    int p = 0; // 指向排序序列
    while (p1 < firstHalf.length && p2 < secondHalf.length) {
        if (firstHalf[p1] <= secondHalf[p2]) { // 相等时取前半部分的,保持稳定性
            arr[p] = firstHalf[p1];
            p1++;
        } else {
            arr[p] = secondHalf[p2];
            p2++;
        }
        p++;
    }
    // 将剩余部分全部添加到arr
    while (p1 < firstHalf.length) {
        arr[p] = firstHalf[p1];
        p++;
        p1++;
    }
    while (p2 < secondHalf.length) {
        arr[p] = secondHalf[p2];
        p++;
        p2++;
    }
}

6. 快速排序

基本思想:将某个元素放置到最终位置,再递归地对左右子序列应用快速排序。

时间复杂度:O(nlog(n))O(nlog(n))

稳定性:并非元素两两比较交换,因此不稳定。

@Override
public void sort(int[] arr) {
    quickSort(arr, 0, arr.length);
}

/**
     * 快速排序
     * @param arr 排序数组
     * @param startInclusive 起始下标(包含)
     * @param endExclusive 终止下标(不包含)
     */
private void quickSort(int[] arr, int startInclusive, int endExclusive) {
    if (startInclusive < endExclusive) {
        int pivotIndex = layDownPivot(arr, startInclusive, endExclusive);
        quickSort(arr, startInclusive, pivotIndex);
        quickSort(arr, pivotIndex + 1, endExclusive);
    }
}

/**
     * 将最后一个元素视作枢轴元素,找到第一个大于枢轴元素的元素及其下标index,如果右侧没有更小的元素,则index就是枢轴最终的下标。
     * 遍历index右侧的元素,更小的元素与index交换,index也右移,表示更小元素移到枢轴元素右侧。
     * @param arr 排序数组
     * @param startInclusive 起始下标(包含)
     * @param endExclusive 终止下标(不包含)
     */
private int layDownPivot(int[] arr, int startInclusive, int endExclusive) {
    int pivot = arr[endExclusive - 1]; // 枢轴元素
    // 找到首个大于枢轴的元素下标
    int p = findFirstBigger(arr, startInclusive, endExclusive, pivot);
    if (p == -1) { // 此时表示枢轴元素已经在最终位置,可以返回了
        return endExclusive - 1;
    }
    int pointer = p++; // 若右侧没有更小的元素,则pointer就是枢轴最终的下标
    // 遍历剩余的元素,若找到小于枢轴的元素,将其与 pointer 交换,并让 pointer++
    while (p < endExclusive) {
        if (arr[p] < pivot) {
            swap(arr, p, pointer);
            pointer++;
        }
        p++;
    }
    // 让枢轴元素前往指定位置
    swap(arr, pointer, endExclusive - 1);
    return pointer;
}

/**
     * 找到首个大于枢轴的元素下标
     */
private int findFirstBigger(int[] arr, int startInclusive, int endExclusive, int pivot) {
    for (int i = startInclusive; i < endExclusive; i++) {
        if (arr[i] > pivot) {
            return i;
        }
    }
    return -1;
}

7. 堆排序

算法思想:首先,在 [0,...,len-1] 范围内建立大根堆,将 arr[0]arr[len-1] 交换,再在 [0,...,len-2] 范围内建立大根堆,直到 [0,1]

时间复杂度:O(nlog(n))O(nlog(n))

稳定性:由于是跳跃比较,并非两两相邻比较,因此是非稳定的。

@Override
public void sort(int[] arr) {
    heapSort(arr);
}

private void heapSort(int[] arr) {
    for (int i = arr.length; i > 0; i--) {
        // 建立[0,i)范围内的大根堆,则下标 0 处的结点值是最大的
        buildMaxHeap(arr, i);
        // 将最大值交换到最右侧
        swap(arr, 0, i - 1);
    }
}

/**
     * 在[0,endExclusive)范围建立大根堆
     * 从最后一个非叶子结点开始向左、向上遍历,看其是否为大根堆,若不是,把大的交换上来,交换下去的结点就不一定能当大根了,需要再比较交换,递归地进行下去。
     *
     */
private void buildMaxHeap(int[] arr, int endExclusive) {
    // 最后一个非叶子结点
    int lastNotLeafNode = endExclusive / 2 - 1;
    for (int i = lastNotLeafNode; i >= 0; i--) {
        compareAndSwap(i, arr, endExclusive);
    }
}

/**
     * 比较并交换 index 及其左右孩子,若进行了交换,则递归地进行下去
     */
private void compareAndSwap(int index, int[] arr, int endExclusive) {
    int largestIndex = findLargestIndex(index, arr, endExclusive); // 找到最大者下标
    if (index != largestIndex) { // 父结点值不是最大的,需要交换
        swap(arr, index, largestIndex);
        compareAndSwap(largestIndex, arr, endExclusive);
    }
}

/**
     * 寻找下标为 defaultIndex 的结点和其左右孩子中最大者的下标
     * @param defaultIndex 父结点下标
     * @param arr 数组
     * @param endExclusive 范围(不包括)
     */
private int findLargestIndex(int defaultIndex, int[] arr, int endExclusive) {
    int largestIndex = defaultIndex;
    int leftChildIndex = 2 * defaultIndex + 1, rightChildIndex = 2 * defaultIndex + 2;
    if (leftChildIndex < endExclusive && arr[leftChildIndex] > arr[largestIndex]) {
        largestIndex = leftChildIndex;
    }
    if (rightChildIndex < endExclusive && arr[rightChildIndex] > arr[largestIndex]) {
        largestIndex = rightChildIndex;
    }
    return largestIndex;
}

8. 计数排序

基本思想:适用于固定范围的整数排序。假设数组最大值为 max,最小值为 min,则创建大小为 max-min+1 的数组 countArr 进行收集操作。遍历原数组,对一个数 numcountArr[num-min]++。遍历 countArr,将收集到的数按顺序添加到 arr

时间复杂度:找到最大最小值O(n)O(n),收集O(n)O(n),释放O(n)O(n),因此时间复杂度O(n)。

稳定性:可以稳定。

@Override
public void sort(int[] arr) {
    int[] minAndMax = findM(arr);
    int min = minAndMax[0], max = minAndMax[1];
    // 创建收集数组
    int[] countArr = new int[max - min + 1];
    for (int num : arr) {
        countArr[num - min]++;
    }
    // 添加到 arr
    int p = 0;
    for (int i = 0; i < countArr.length; i++) {
        while (countArr[i] > 0) {
            arr[p++] = i + min;
            countArr[i]--;
        }
    }
}

/**
     * 查找最值
     */
private int[] findM(int[] arr) {
    int[] minAndMax = new int[2];
    int minIndex = 0, maxIndex = 0;
    for (int i = 0; i < arr.length; i++) {
        if (arr[i] < arr[minIndex]) {
            minIndex = i;
            continue;
        }
        if (arr[i] > arr[maxIndex]) {
            maxIndex = i;
        }
    }
    minAndMax[0] = arr[minIndex];
    minAndMax[1] = arr[maxIndex];
    return minAndMax;
}

9. 桶排序

基本思想:将元素放入指定数目的桶中,对每个桶中的元素分别进行排序,最后进行合并。

时间复杂度:创建桶O(k),获取最大最小值O(n),向桶中添加元素O(n),各桶排序O(k * (n/k)log(n/k))=O(nlog(n/k)),倾倒O(n),因此复杂度O(k+n+nlog(n/k))

稳定性:可以稳定。

@Override
public void sort(int[] arr) {
    bucketSort(arr, 3);
}

private void bucketSort(int[] arr, int bucketSize) {
    // 创建桶
    ArrayList<Integer>[] bucket = initializeBucket(bucketSize);
    // 计算桶容量和最小值
    int[] boundaryAndMin = countBoundary(arr, bucketSize);
    int boundary = boundaryAndMin[0], min = boundaryAndMin[1];
    // 向桶中添加元素
    fillBucket(arr, bucket, bucketSize, min, boundary);
    // 桶中元素排序
    sortEachBucket(bucket);
    // 倾倒桶中元素到数组
    dump(arr, bucket);
}

private ArrayList<Integer>[] initializeBucket(int bucketSize) {
ArrayList<Integer>[] bucket = new ArrayList[bucketSize];
for (int i = 0; i < bucketSize; i++) {
    bucket[i] = new ArrayList<>();
}
return bucket;
}

private void dump(int[] arr, ArrayList<Integer>[] bucket) {
    int i = 0;
    for (ArrayList<Integer> list : bucket) {
        for (Integer num : list) {
            arr[i++] = num;
        }
    }
}

/**
     * 计算桶容量
     */
private int[] countBoundary(int[] arr, int bucketSize) {
    int[] minAndMax = findMaxAndMin(arr);
    int min = minAndMax[0], max = minAndMax[1];
    int boundary = (max - min) / bucketSize;
    if (boundary < 1) {
        throw new IllegalArgumentException("桶数量设置不合理!");
    }
    return new int[]{(max - min) / bucketSize, min}; // i - min / boundary 就是元素桶的下标
}

/**
     * 对每个桶进行排序
     */
private void sortEachBucket(ArrayList<Integer>[] bucket) {
    for (ArrayList<Integer> singleBucket : bucket) {
        Collections.sort(singleBucket);
    }
}

/**
     * 填充桶
     *
     * @param arr        数组
     * @param bucket     桶
     * @param bucketSize 桶大小
     * @param min        元素最小值
     */
private void fillBucket(int[] arr, ArrayList<Integer>[] bucket, int bucketSize, int min, int boundary) {
    for (int num : arr) {
        int countedIndex = (num - min) / boundary;
        int realIndex = countedIndex == bucketSize ? bucketSize - 1 : countedIndex;
        bucket[realIndex].add(num);
    }
}

/**
     * 寻找数组中的最大值和最小值
     */
private int[] findMaxAndMin(int[] arr) {
    int minIndex = 0, maxIndex = 0;
    for (int i = 0; i < arr.length; i++) {
        if (arr[i] < arr[minIndex]) {
            minIndex = i;
            continue;
        }
        if (arr[i] > arr[maxIndex]) {
            maxIndex = i;
        }
    }
    return new int[]{arr[minIndex], arr[maxIndex]};
}

10. 基数排序

基本思想:首先计算出数组中最大值的位数 digits,对数组进行 digits 轮排序,即先按个位排序、再按十位排序...,直到按最大位排序。只适用于非负整数排序

时间复杂度:对每一轮排序,都需要先收集后释放所有元素,因此时间复杂度O(k)O(k)kk为最大元素的位数。

稳定性:稳定。

@Override
public void sort(int[] arr) {
    // 计算数组最大值的位数
    int digits = countDigit(arr);
    // 基数桶,横坐标:0~9表示位值,每行都存储了一些元素
    int[][] buckets = new int[10][arr.length];
    // 标记每个基数桶中存储了多少个元素
    int[] bucketCount = new int[10];
    // 对数组进行 digits 轮排序
    for (int i = 0, n = 1; i < digits; i++, n *= 10) {
        // 收集
        for (int num : arr) {
            int bucketIndex = (num / n) % 10;
            buckets[bucketIndex][bucketCount[bucketIndex]++] = num;
        }
        // 释放
        int index = 0;
        for (int j = 0; j < buckets.length; j++) {
            for (int k = 0; k < bucketCount[j]; k++) {
                arr[index++] = buckets[j][k];
            }
        }
        // 清空 bucketCount
        clearBucket(bucketCount);
    }
}

private void clearBucket(int[] bucketCount) {
    for (int i = 0; i < bucketCount.length; i++) {
        bucketCount[i] = 0;
    }
}

/**
     * 计算数组中最大值的位数
     * @param arr
     * @return
     */
private int countDigit(int[] arr) {
    // 找到最大值
    int max = findMax(arr);
    // 获取位数
    return getDigits(max);
}

/**
     * 获取 num 的位数
     */
private int getDigits(int num) {
int digits = 1;
while (num > 10) {
    num /= 10;
    digits++;
}
return digits;
}

/**
     * 找到数组中的最大值
     */
private int findMax(int[] arr) {
    int max = arr[0];
    for (int num : arr) {
        max = num > max ? num : max;
    }
    return max;
}