【算法】常见排序

201 阅读5分钟

1. 直接插入排序

1.1 思路

  • 把n个待排序的元素看成是一个有序表和一个无序表
  • 开始时,有序表只有一个元素(即第一个元素),无序表有n-1个元素(即后面的元素)
  • 排序过程中,每次从无序表取出第一个元素,把它按序插入到有序表中,以此更新有序表
  • 重复上一步操作,直到无序表没有元素为止

1.2 代码

public class InsertSort {
    
    public void insertSort(int[] arr) {
        if (arr == null || arr.length == 0 || arr.length == 1) {
            return;
        }
        for (int i = 1; i < arr.length; i++) {
            // 待插入值
            int insertVal = arr[i];
            // 待插入的位置,初始化为待插入值的前一个位置
            int insertIndex = i - 1;

            // 1. insertIndex >= 0 防止越界
            // 2. insertVal < arr[insertIndex] 说明待插入值还没有找到适当的插入位置
            while (insertIndex >= 0 && insertVal < arr[insertIndex]) {
                // 将arr[insertIndex]后移
                arr[insertIndex + 1] = arr[insertIndex];
                insertIndex--;
            }
            // 退出while循环时,待插入位置即为insertIndex + 1
            arr[insertIndex + 1] = insertVal;
        }
    }
}

2. 希尔排序

  插入排序存在的问题,如果数组 arr = {2,3,4,5,6,1},当待插入的数是1时,后移的次数较多,影响性能。即插入排序有以下缺点:当待插入的数较小时,后移的次数会较多。希尔排序是插入排序的改良。

2.1 思路

  • 定义一个变量gap,初始化为队列长度除以2
  • 把元素分成gap组,每组的元素使用插入排序
  • gap变量继续除以2,再分成gap组,对每组使用插入排序
  • 当gap变量变为1时,对整个队列使用插入排序,算法终止

注:挑选某些元素并把它们放到一组的这步操作有一定规律,如下图所示 032-希尔排序.jpg

2.2 代码

public class ShellSort {

    public void shellSort(int[] arr) {
        if (arr == null || arr.length == 0 || arr.length == 1) {
            return;
        }
        int gap = arr.length / 2;
        while (gap > 0) {
            for (int i = gap; i < arr.length; i++) {
                int investVal = arr[i];
                int investIndex = i - gap;
                while (investIndex >= 0 && investVal < arr[investIndex]) {
                    arr[investIndex + gap] = arr[investIndex];
                    investIndex -= gap;
                }
                arr[investIndex + gap] = investVal;
            }
            gap = gap / 2;
        }
    }
}

3. 简单选择排序

3.1 思路

  • 第一次从arr[0] ~ arr[n-1]中找到最小值,与arr[0]交换

  • 第二次从arr[1] ~ arr[n-1]中找到最小值,与arr[1]交换

  • 以此类推,第 i 次从arr[i-1] ~ arr[n-1]中找到最小值,与arr[i-1]交换

3.2 代码

public class SelectSort {

    public void selectSort(int[] arr) {
        if (arr == null || arr.length == 0 || arr.length == 1) {
            return;
        }
        for (int i = 0; i < arr.length - 1; i++) {
            int minIndex = i;
            int min = arr[i];
            for (int j = i + 1; j < arr.length; j++) {
                if (arr[j] < min) {
                    minIndex = j;
                    min = arr[j];
                }
            }
            if (minIndex != i) {
                arr[minIndex] = arr[i];
                arr[i] = min;
            }
        }
    }
}

4. 堆排序

  堆排序是利用这种数据结构设计的排序算法,是一种选择排序。堆的定义如下:

  • 堆是一棵完全二叉树
  • 每个节点的值都大于等于其左右孩子的值(这种称为大顶堆,反之称为小顶堆),注意,并没有定义左右孩子节点之间的大小关系
  • 可以用数组存储一个堆,如下是一个大顶堆的关系
// i表示第i个节点,从0开始,它的左右孩子节点数则为 2*i+1 和 2*i+2
arr[i] >= arr[2*i + 1] && arr[i] >= arr[2*1 + 2]

4.1 思路

  • 将待排序队列构造成一个大顶堆
  • 整个队列的最大值就是堆的根节点
  • 将最大值与末尾元素交换
  • 剩余的n-1个元素重新构造成一个堆,如此反复操作得到一个有序队列

4.2 代码

public class HeapSort {

    public void heapSort(int[] arr) {
        if (arr == null || arr.length == 0 || arr.length == 1) {
            return;
        }
        // 1、从倒数第一个非叶子节点开始调整,从下到上,从右到左,构造一个大顶堆
        for (int i = arr.length / 2 - 1; i >= 0; i--) {
            convertToMaxHeap(arr, i, arr.length);
        }
        // 2、把堆顶元素与末尾元素交换
        // 3、然后让新的堆顶元素"下沉"到适当位置,重新调整成一个大顶堆结构
        int temp;
        for (int j = arr.length - 1; j > 0; j--) {
            temp = arr[0];
            arr[0] = arr[j];
            arr[j] = temp;

            convertToMaxHeap(arr, 0, j);
        }
    }
    
    /**
     * 把以node当作根节点的树调整成一个大顶堆
     *
     * @param arr    原数组
     * @param node   把该节点作为根的树
     * @param length 调整的元素数量
     */
    private void convertToMaxHeap(int[] arr, int node, int length) {
        int temp = arr[node];
        // 定位到左孩子
        for (int k = getLeftChild(node); k < length; k = getLeftChild(k)) {
            // 如果左孩子小于右孩子
            if ((k + 1 < length) && (arr[k] < arr[k + 1])) {
                k++;
            }
            // 此时k指向左右孩子中最大的那个
            if (arr[k] > temp) {
                arr[node] = arr[k];
                node = k;
            } else {
                break;
            }
        }
        arr[node] = temp;
    }

    private int getLeftChild(int node) {
        return node * 2 + 1;
    }
}

5. 冒泡排序

5.1 思路

  • 从下标较小的元素开始,依次比较相邻元素的值,若发现逆序则交换,使值较大的元素逐渐从前往后移动,就像水底的泡泡往上冒一样

  • 如果一趟比较下来没有进行过交换,则说明原队列有序,此时不需要继续下一趟比较

5.2 代码

public class BubbleSort {

    public void bubbleSort(int[] arr) {
        if (arr == null || arr.length == 0 || arr.length == 1) {
            return;
        }
        // 第i+1趟
        for (int i = 0; i < arr.length - 1; i++) {
            // 是否交换过元素的标志
            boolean flag = true;
            // 依次比较两个相邻元素
            for (int j = 0; j < arr.length - 1 - i; j++) {
                if (arr[j] > arr[j + 1]) {
                    exchange(arr, j, j + 1);
                    flag = false;
                }
            }
            if (flag) {
                break;
            }
        }
    }

    private void exchange(int[] arr, int i, int j) {
        int temp = arr[i];
        arr[i] = arr[j];
        arr[j] = temp;
    }
}

6. 快速排序

6.1 思路

  • 快速排序是对冒泡排序的改进
  • 其思路是:将队列分割成独立的两部门,其中一部分的所有数据都比另一部分的数据小,然后再按此方法对这两部分数据分别进行快速排序
  • 整个排序过程使用递归进行,直到整个队列有序为止

6.2 代码

public class QuickSort {
    
    public void quickSort(int[] arr) {
        if (arr == null || arr.length == 0 || arr.length == 1) {
            return;
        }
        quickSort(arr, 0, arr.length - 1);
    }

    private void quickSort(int[] arr, int left, int right) {
        if (left >= right) {
            return;
        }
        int l = left;
        int r = right;
        int pivot = arr[l];
        while (l < r) {
            // 找到一个比pivot小的数
            while (l < r && arr[r] >= pivot) {
                r--;
            }
            if (l < r) {
                arr[l] = arr[r];
            }
            // 找到一个比pivot大的数
            while (l < r && arr[l] <= pivot) {
                l++;
            }
            if (l < r) {
                arr[r] = arr[l];
            }

            if (l >= r) {
                arr[l] = pivot;
            }
        }
        quickSort(arr, left, r - 1);
        quickSort(arr, r + 1, right);
    }
}

7. 归并排序

7.1 思路

  • 使用分治策略,把一个需要排序的大队列,拆解成一个个小队列,排序这些小队列,再合成新的较大队列 032-归并排序.png

7.2 代码

public class MergeSort {

    public void mergeSort(int[] arr) {
        if (arr == null || arr.length == 0 || arr.length == 1) {
            return;
        }
        // 提前创建一个临时数组,避免在递归里创建数组
        int[] temp = new int[arr.length];
        mergeSort(arr, 0, arr.length - 1, temp);
    }

    private void mergeSort(int[] arr, int left, int right, int[] temp) {
        if (left < right) {
            int mid = (left + right) / 2;
            mergeSort(arr, left, mid, temp);
            mergeSort(arr, mid + 1, right, temp);
            // 将左右两个有序小队列合并
            merge(arr, left, mid, right, temp);
        }
    }

    private void merge(int[] arr, int start, int mid, int end, int[] temp) {
        int i = start;
        int j = mid + 1;

        int t = 0;
        // 排序两个小队列
        while (i <= mid && j <= end) {
            if (arr[i] < arr[j]) {
                temp[t++] = arr[i++];
            } else {
                temp[t++] = arr[j++];
            }
        }
        while (i <= mid) {
            temp[t++] = arr[i++];
        }
        while (j <= end) {
            temp[t++] = arr[j++];
        }
        // 新的较大有序队列覆盖原队列
        t = 0;
        for (int k = start; k <= end; k++) {
            arr[k] = temp[t++];
        }
    }
}

8. 基数排序

8.1 思路

  • 将所有待比较的数值统一为相同的数位长度,数位较短的前面补零

  • 从最低位开始,依次进行排序

  • 具体思路可参考该视频

8.2 代码

public class RadixSort {

    public void radixSort(int[] arr) {
        if (arr == null || arr.length == 0 || arr.length == 1) {
            return;
        }
        // 定义一个二维数组,表示10个桶
        int[][] bucket = new int[10][arr.length];
        // 记录每个桶中实际存放的有效数据的数量
        int[] bucketCount = new int[10];
        // 得到队列中最大数的位数
        int maxLength = getMaxLengthOfArr(arr);
        for (int i = 1, n = 1; i <= maxLength; i++, n *= 10) {
            for (int num : arr) {
                // 第一轮取个位,第二轮取十位,第三轮取百位...
                int digit = num / n % 10;

                bucket[digit][bucketCount[digit]] = num;
                bucketCount[digit]++;
            }
            coverArrFromBucket(arr, bucket, bucketCount);
        }
    }

    /**
     * 把桶里面的数据覆盖到原队列中
     */
    private void coverArrFromBucket(int[] arr, int[][] bucket, int[] bucketCount) {
        int index = 0;
        // 遍历这10个桶
        for (int i = 0; i < 10; i++) {
            // 如果桶里没数据,跳过
            if (bucketCount[i] == 0) {
                continue;
            }
            for (int j = 0; j < bucketCount[i]; j++) {
                arr[index++] = bucket[i][j];
            }
            // 桶里的元素数量清零
            bucketCount[i] = 0;
        }
    }

    /**
     * 得到队列中最大数的位数
     */
    private int getMaxLengthOfArr(int[] arr) {
        int max = arr[0];
        for (int i = 1; i < arr.length; i++) {
            max = Math.max(max, arr[i]);
        }
        return String.valueOf(max).length();
    }
}

9. 总结

032-排序比较.png

  • 稳定:如果a原本在b前面,而a=b,排序之后a仍然在b的前面
  • 不稳定:如果a原本在b的前面,而a=b,排序之后a可能会出现在b的后面
  • 内排序:所有排序操作都在内存中完成
  • 外排序:由于数据太大,因此把数据放在磁盘中,而排序通过磁盘和内存的数据传输才能进行
  • n:数据规模
  • k:“桶“的个数
  • In-place:不占用额外内存
  • Out-place:占用额外内存

10. 相关链接