Skip to content

常见排序算法

分析排序算法

  • 执行效率
    • 最好, 最坏, 平均时间复杂度
    • 当时间复杂度相同时, 要考虑系数, 常量, 低阶
    • 比较次数或交换次数

1744803993470

  • 内存消耗, 原地排序(特指空间复杂度为O(1)的算法)
  • 稳定性, 如果待排序的序列中存在值相等的元素, 经过排序之后, 相等元素之间原有的先后顺序不变
    • 举例, 要给订单按照金额从小到大排序, 对于金额相同, 按照下单时间从早到晚排序; 若先按照金额对订单进行排序, 再遍历排序之后的订单, 对于金额相同的小区间再按照下单时间排序, 实现会教复杂; 借助稳定排序算法, 可先按下单时间给订单排序, 再用稳定排序算法对订单金额重新排序即可

冒泡排序

  • 原理: 从左到右遍历数组, 每次遍历中依次比较相邻元素, 若不满足大小关系则互换位置, 事实上每次冒泡都会找到当下最大/小的元素移至右侧合适的位置
Java
publicclassBubbleSort {
    // 测试冒泡排序的方法
publicstaticvoidmain(String[] args) {
        int[] arrayToSort = {64, 34, 25, 12, 22, 11, 90};
        System.out.println("排序前:");
        System.out.println(Arrays.toString(arrayToSort));

        bubbleSort(arrayToSort); // 调用冒泡排序

System.out.println("排序后:");
        System.out.println(Arrays.toString(arrayToSort));
    }

    privatestaticvoidbubbleSort(int[] array) {
        intn = array.length;
        booleanswapped = false;
        for(inti = 0; i < n - 1; i++) {
            for(intj = 0; j < n - 1 - i; j++) {
                if(array[j] > array[j + 1]) {
                    inttemp = array[j];
                    array[j] = array[j + 1];
                    array[j + 1] = temp;
                    swapped = true;
                }
            }
            if(!swapped) {
                break;
            }
        }
    }
}

1744804021742

1744804030041

  • 优化: 当某次冒泡操作已经没有数据交换时, 说明已经达到完全有序, 不用再继续执行后续的冒泡操作

1744804055462

  • 分析
    • 空间复杂度O(1)/原地排序算法

    • 稳定排序

    • 时间复杂度, 平均时间复杂度为

      $$ O(n^2) $$

1744804088044

插入排序

  • 原理: 将数组分为已排序区和未排序区, 初始时只有数组的第一个元素是已排序区; 取未排序区间中的元素, 在已排序区中找到合适的位置插入, 重复这个过程, 直到未排序区中元素为空
Java
publicclassInsertionSort {
    publicstaticvoidmain(String[] args) {
        int[] arrayToSort = {12, 11, 13, 5, 6};
        System.out.println("排序前:");
        System.out.println(Arrays.toString(arrayToSort));
        insertionSort(arrayToSort); // 调用插入排序

System.out.println("排序后:");
        System.out.println(Arrays.toString(arrayToSort));
    }

    privatestaticvoidinsertionSort(int[] arr) {
        intn = arr.length;
        for(inti = 1; i < n; i++) {
            // i是未排序区待排序的第一个元素的下标, key是从未排序区取出的第一个待排序的元素
intkey = arr[i];

            // j是有序区的下标, 从末尾开始对比
intj = i - 1;
            for(; j >= 0; j--) {
                if(arr[j] > key) {
                    // j位置的元素比key大, 则往右移
arr[j + 1] = arr[j];
                } else{
                    break;
                }
            }
            // 直至j位置的元素比key小, 那么key就放在j+1的位置
arr[j + 1] = key;
        }
    }
}

1744804107235

  • 分析
    • 原地排序

    • 稳定排序

    • 平均时间复杂度** $$ O(n^2) $$

      **

选择排序

  • 原理: 也是将数组分为已排序区和未排序区, 每次会从未排序区找到最小的元素, 将其放到已排序区的末尾
Java
publicclassSelectionSort {
    publicstaticvoidmain(String[] args) {
        int[] arrayToSort = {64, 25, 12, 22, 11};
        System.out.println("排序前:");
        System.out.println(Arrays.toString(arrayToSort));

        selectionSort(arrayToSort); // 调用选择排序

System.out.println("排序后:");
        System.out.println(Arrays.toString(arrayToSort));
    }

    privatestaticvoidselectionSort(int[] arr) {
        intn = arr.length;
        for(inti = 0; i < n - 1; i++) {
            // 假设当前元素是最小的
intminIndex = i;
            for(intj = i + 1; j < n; j++) {
                if(arr[j] < arr[minIndex]) {
                    // 从未排序部分寻找最小元素
minIndex = j;
                }
            }

            // 如果找到了新的最小值, 则交换位置
if(minIndex != i) {
                inttemp = arr[i];
                arr[i] = arr[minIndex];
                arr[minIndex] = temp;
            }
        }
    }
}

1744804124770

  • 分析
    • 原地排序

    • 不稳定, 比如 5,8,5,2,9 这样一组数据,使用选择排序算法来排序的话,第一次找到最小元素 2,与第一个 5 交换位置,那第一个 5 和中间的 5 顺序就变了,所以就不稳定了

    • 最好情况时间复杂度、最坏情况和平均情况时间复杂度都为 ** $$ O(n^2) $$

      **

归并排序

  • 原理: 把数组从中间分成前后两部分,然后对前后两部分分别排序,再将排好序的两部分合并在一起,这样整个数组就都有序了

1744804140689

  • 使用递归来实现归并排序, 写递归技巧, 得到递推公式+找到终止条件
Java
递推公式:
merge_sort(p…r) = merge(merge_sort(p…q), merge_sort(q+1…r))

终止条件:
p >= r 不用再继续分解
q=(p+r)/2
Java
publicclassMergeSort {
    // 主方法用于测试
publicstaticvoidmain(String[] args) {
        int[] array = {38, 27, 43, 3, 9, 82, 10};
        System.out.println("Original array:");
        System.out.println(Arrays.toString(array));

        mergeSort(array, 0, array.length - 1);

        System.out.println("Sorted array:");
        System.out.println(Arrays.toString(array));
    }

    // 归并排序方法
publicstaticvoidmergeSort(int[] array, intleft, intright) {
        if(left < right) {
            // 找到中间点
intmiddle = (left + right) / 2;

            // 排序左半部分
            mergeSort(array, left, middle);
            // 排序右半部分
            mergeSort(array, middle + 1, right);

            // 合并两部分
            merge(array, left, middle, right);
        }
    }

    // 合并两个已经排序的子数组
publicstaticvoidmerge(int[] array, intleft, intmiddle, intright) {
        // 创建临时数组来存储左右两部分
intn1 = middle - left + 1;
        intn2 = right - middle;

        // 创建临时数组
int[] L = newint[n1];
        int[] R = newint[n2];

        // 将数据复制到临时数组 L[] 和 R[]
for(inti = 0; i < n1; ++i)
            L[i] = array[left + i];
        for(intj = 0; j < n2; ++j)
            R[j] = array[middle + 1 + j];

        // 合并临时数组回原数组
inti = 0, j = 0;
        intk = left;
        while(i < n1 && j < n2) {
            if(L[i] <= R[j]) {
                array[k] = L[i];
                i++;
            } else{
                array[k] = R[j];
                j++;
            }
            k++;
        }

        // 检查是否还有剩余元素在 L[]
while(i < n1) {
            array[k] = L[i];
            i++;
            k++;
        }

        // 检查是否还有剩余元素在 R[]
while(j < n2) {
            array[k] = R[j];
            j++;
            k++;
        }
    }
}
  • 分析
    • 稳定排序

    • 最好, 最坏, 平均时间复杂度都是** $$ O(nlogn) $$

      **

    • 空间复杂度为O(n)

快速排序

  • 原理:
    • 如果要排序数组中下标从 p 到 r 之间的一组数据,我们选择 p 到 r 之间的任意一个数据作为 pivot(分区点)。
    • 我们遍历 p 到 r 之间的数据,将小于 pivot 的放到左边,将大于 pivot 的放到右边,将 pivot 放到中间。经过这一步骤之后,数组 p 到 r 之间的数据就被分成了三个部分,前面 p 到 q-1 之间都是小于 pivot 的,中间是 pivot,后面的 q+1 到 r 之间是大于 pivot 的。

1744804167312

  • 根据分治、递归的处理思想,我们可以用递归排序下标从 p 到 q-1 之间的数据和下标从 q+1 到 r 之间的数据,直到区间缩小为 1,就说明所有的数据都有序了。
  • 如果我们用递推公式来将上面的过程写出来的话,就是这样:
Java
递推公式:
quick_sort(p…r) = quick_sort(p…q-1) + quick_sort(q+1… r)

终止条件:
p >= r
  • 上面的递归是容易写的, 主要是为了实现原地排序(空间复杂度O(1)), 分区方法比较复杂, 思路类似于选择排序, 通过游标 i 把 A[p...r-1]分成两部分。A[p...i-1]的元素都是小于 pivot 的,我们暂且叫它“已处理区间”,A[i...r-1]是“未处理区间”。我们每次都从未处理的区间 A[i...r-1]中取一个元素 A[j],与 pivot 对比,如果小于 pivot,则将其加入到已处理区间的尾部,也就是 A[i]的位置。
Java
publicclassQuickSort {
    // 主方法用于测试
publicstaticvoidmain(String[] args) {
        int[] array = {38, 27, 43, 3, 9, 82, 10};
        System.out.println("Original array:");
        System.out.println(Arrays.toString(array));

        quickSort(array, 0, array.length - 1);

        System.out.println("Sorted array:");
        System.out.println(Arrays.toString(array));
    }

    // 快速排序方法
publicstaticvoidquickSort(int[] array, intlow, inthigh) {
        if(low < high) {
            // pi 是分区索引,array[pi] 已经排好序
intpi = partition(array, low, high);

            // 分别对左右子数组进行排序
            quickSort(array, low, pi - 1);  // 排序左半部分
            quickSort(array, pi + 1, high); // 排序右半部分
}
    }

    // 分区方法,返回分区索引
privatestaticintpartition(int[] array, intlow, inthigh) {
        intpivot = array[high]; // 选择最后一个元素作为基准
inti = low - 1; // 较小元素的索引

for(intj = low; j < high; j++) {
            // 如果当前元素小于或等于pivot
if(array[j] <= pivot) {
                i++;

                // 交换array[i]和array[j]
inttemp = array[i];
                array[i] = array[j];
                array[j] = temp;
            }
        }

        // 交换array[i+1]和array[high] (或pivot)
inttemp = array[i + 1];
        array[i + 1] = array[high];
        array[high] = temp;

        returni + 1;
    }

}
  • 分析
    • 非稳定排序

    • 平均时间复杂度**

      $$ O(nlogn) $$

      , 最快时间复杂度

      $$ O(n^2) $$

      **

    • 空间复杂度O(1)

  • 快排和归并用的都是分治思想,递推公式和递归代码也非常相似,那它们的区别在哪里呢?
    • 归并排序的处理过程是由下到上的,先处理子问题,然后再合并。而快排正好相反,它的处理过程是由上到下的,先分区,然后再处理子问题
    • 归并是稳定的, 但非原地排序; 快拍不稳定, 但通过巧妙的原地分区函数, 可以实现原地排序, 解决了归并排序内存占用多的问题

1744804189546

总结

  1. 冒泡排序:通过重复地遍历列表,比较相邻元素并交换顺序错误的元素,每次遍历将最大(或最小)的元素“浮”到正确位置
  2. 插入排序:构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入
  3. 选择排序:在未排序序列中找到最小(或最大)元素,存放到排序序列的起始位置,然后再从剩余未排序元素中继续寻找最小(或最大)元素,然后放到已排序序列的末尾。
  4. 快速排序:通过选定一个“基准”元素,将数组分为两部分,一部分比基准小,一部分比基准大,然后递归地对这两部分进行快速排序。
  5. 归并排序:将数组分成两半,分别对它们进行排序,然后将排序好的两半合并成一个有序数组。
  6. 桶排序,针对排序内存不足的情况,可先将序列分桶,每个桶内再排序,最后合并
  7. 计数排序,当排序的范围不大的时候,假设范围K,可直接K个桶,如10万学生高考成绩排序,学生数量大,但高考成绩范围不大,可直接按高考满分成绩分桶
  8. 基数排序, 适用于元素可按位划分排序的情况,比如手机号排序,可先比较低位,再比较高位; 比如姓名排序,可按字符编码,先排低位, 再排高位