Bootstrap

【Java】面试中遇到的两个排序

快速排序(QuickSort)

快速排序(QuickSort)是一种基于分治法(Divide and Conquer)的排序算法。其基本思想是通过一个“分区”操作将一个大的数组分成两个子数组,递归地对这两个子数组进行排序。以下是快速排序的详细步骤和过程。

快速排序的算法过程:

  1. 选择一个基准元素:从数组中选择一个元素作为基准元素(pivot)。通常选择第一个元素、最后一个元素、或者随机选一个元素。我们以选择数组最后一个元素作为基准。

  2. 分区操作:将数组分为两部分:

    • 一部分是所有小于基准元素的元素。
    • 另一部分是所有大于基准元素的元素。
    • 这时候,基准元素已经排好序了,它处于最终位置。
  3. 递归排序子数组:对基准元素左边和右边的子数组分别进行递归排序。重复上述操作,直到数组完全排序。

快速排序的详细步骤:

  1. 选择基准:我们选择数组的最后一个元素作为基准。

  2. 分区操作

    • 用一个指针(通常称为 i)标记已经排好序的部分。
    • 遍历数组中的每个元素,与基准元素比较:
      • 如果当前元素小于或等于基准元素,则将该元素与 i 之后的位置交换,i 右移。
      • 如果当前元素大于基准元素,则继续遍历。
    • 最后,将基准元素与 i + 1 位置的元素交换,确保基准元素在其正确的位置上。
  3. 递归排序:对基准元素左边的子数组和右边的子数组分别进行递归调用快速排序,直到子数组只有一个元素或者为空。

快速排序的Java实现:

public class QuickSort {

    // 快速排序函数
    public static void quickSort(int[] arr, int low, int high) {
        if (low < high) {
            // 1. 执行分区操作,获取基准元素的最终位置
            int pivotIndex = partition(arr, low, high);
            
            // 2. 递归地对基准左边的子数组进行排序
            quickSort(arr, low, pivotIndex - 1);
            
            // 3. 递归地对基准右边的子数组进行排序
            quickSort(arr, pivotIndex + 1, high);
        }
    }

    // 分区操作,返回基准元素的位置
    private static int partition(int[] arr, int low, int high) {
        int pivot = arr[high];  // 选择数组中的最后一个元素作为基准
        int i = low - 1;        // i 表示小于基准的元素区的最后一个位置

        for (int j = low; j < high; j++) {
            // 如果当前元素小于等于基准
            if (arr[j] <= pivot) {
                i++; // 增加小于等于基准的元素区的边界
                swap(arr, i, j);  // 交换当前元素和小于等于基准的元素区的下一个位置的元素
            }
        }

        // 将基准元素放到正确的位置
        swap(arr, i + 1, high);

        return i + 1; // 返回基准元素的位置
    }

    // 交换两个元素
    private static void swap(int[] arr, int i, int j) {
        int temp = arr[i];
        arr[i] = arr[j];
        arr[j] = temp;
    }

    // 测试代码
    public static void main(String[] args) {
        int[] arr = { 10, 7, 8, 9, 1, 5 };
        System.out.println("排序前:");
        for (int num : arr) {
            System.out.print(num + " ");
        }

        // 调用快速排序函数
        quickSort(arr, 0, arr.length - 1);

        System.out.println("\n排序后:");
        for (int num : arr) {
            System.out.print(num + " ");
        }
    }
}

步骤解析:

  1. quickSort 函数:这是快速排序的主函数,接收数组、当前范围的低索引和高索引。在每次调用时,quickSort 会对数组的某个子部分进行排序,直到该部分长度为 1 或 0,说明已经排序完成。

  2. partition 函数

    • 选择基准元素 pivot 为当前子数组的最后一个元素(arr[high])。
    • 使用一个指针 i 来标记小于等于基准元素的元素的边界。
    • 遍历数组,对于每个元素,如果它小于或等于基准元素,则将它与指针 i 之后的位置的元素交换。
    • 在遍历完成后,将基准元素与 i+1 位置的元素交换,这样基准元素就被放置在它的正确位置上。
  3. swap 函数:简单的交换数组中的两个元素。

  4. main 函数:演示如何调用 quickSort 并打印排序前后的数组。

快速排序的性能分析:

  • 平均时间复杂度O(n log n),其中 n 是数组的长度。在平均情况下,数组被分割成大约两半,递归调用深度为 log n,每次分区操作的时间复杂度为 O(n)

  • 最坏时间复杂度O(n^2),当数组已经是有序或逆序时,快速排序可能会退化成冒泡排序,导致每次只减少一个元素,递归的深度为 n

  • 空间复杂度O(log n),递归调用的栈空间。

举例说明:

假设我们有数组 [10, 7, 8, 9, 1, 5],基准元素是 5

  1. 第一次分区

    • 选择基准 5
    • 将小于等于 5 的元素移到左边,大于 5 的元素移到右边。分区后,数组变为 [1, 5, 8, 9, 7, 10]5 被放置在正确的位置,即索引 1
  2. 递归排序

    • 对左子数组 [1] 和右子数组 [8, 9, 7, 10] 分别进行递归排序。
    • 对右子数组 [8, 9, 7, 10] 继续执行快速排序。

通过递归和分区操作,数组最终会完全排序。

总结:

快速排序是一种高效的排序算法,特别适用于大数据集。尽管最坏情况下时间复杂度是 O(n^2),但通过选择合适的基准和优化策略,快速排序在实际应用中表现通常非常好。


归并排序(Merge Sort)

归并排序(Merge Sort)是一种基于分治法的排序算法。其基本思想是将一个大问题分解成两个小问题,分别解决这两个小问题,然后将解决后的结果合并,得到最终的排序结果。

归并排序的关键是 分解合并

  • 分解:将数组分成两个子数组,分别对它们进行归并排序。
  • 合并:合并两个已排序的子数组,得到一个排序好的数组。

归并排序的算法过程:

  1. 分解:将待排序数组分成两半,递归地对每个子数组进行排序,直到每个子数组只有一个元素(或者为空数组)。这时候单个元素的数组已经是有序的。

  2. 合并:通过合并已排序的子数组,将它们合并为一个更大的有序数组。这个过程是归并排序的核心。合并时,通过比较两个子数组的元素,将较小的元素放入新数组中,直到一个子数组元素全部合并完,再将另一个子数组剩下的元素直接合并。

  3. 递归过程:不断地将数组分成两半并递归,直到每个子数组只有一个元素,然后从最底层开始,将这些小的有序子数组合并成更大的有序子数组,直到合并为整个排序好的数组。

归并排序的详细步骤:

  1. 递归分解:不断地将数组分割成两部分,直到每部分只包含一个元素(此时单个元素已经有序)。

  2. 合并两个有序子数组:利用一个合并函数将两个已经排序的子数组合并成一个有序数组。

  3. 返回合并结果:合并后的数组就是排序后的数组。

归并排序的Java实现:

public class MergeSort {

    // 主函数,调用归并排序
    public static void mergeSort(int[] arr) {
        if (arr.length < 2) {
            return;  // 如果数组长度小于2,直接返回
        }
        mergeSort(arr, 0, arr.length - 1);
    }

    // 递归的归并排序函数,low 和 high 是当前处理的数组范围
    private static void mergeSort(int[] arr, int low, int high) {
        if (low < high) {
            // 找到中间索引
            int mid = (low + high) / 2;

            // 递归排序左子数组
            mergeSort(arr, low, mid);

            // 递归排序右子数组
            mergeSort(arr, mid + 1, high);

            // 合并两个有序子数组
            merge(arr, low, mid, high);
        }
    }

    // 合并两个已排序的子数组
    private static void merge(int[] arr, int low, int mid, int high) {
        // 创建临时数组来存放合并结果
        int[] temp = new int[high - low + 1];
        int i = low;  // 左子数组的起始位置
        int j = mid + 1; // 右子数组的起始位置
        int k = 0;  // 临时数组的索引

        // 合并两个有序子数组
        while (i <= mid && j <= high) {
            if (arr[i] <= arr[j]) {
                temp[k++] = arr[i++];  // 左子数组的元素小,放入临时数组
            } else {
                temp[k++] = arr[j++];  // 右子数组的元素小,放入临时数组
            }
        }

        // 复制左子数组剩余的元素
        while (i <= mid) {
            temp[k++] = arr[i++];
        }

        // 复制右子数组剩余的元素
        while (j <= high) {
            temp[k++] = arr[j++];
        }

        // 将临时数组中的内容拷贝回原数组
        for (int m = 0; m < temp.length; m++) {
            arr[low + m] = temp[m];
        }
    }

    // 测试代码
    public static void main(String[] args) {
        int[] arr = { 10, 7, 8, 9, 1, 5 };
        System.out.println("排序前:");
        for (int num : arr) {
            System.out.print(num + " ");
        }

        // 调用归并排序
        mergeSort(arr);

        System.out.println("\n排序后:");
        for (int num : arr) {
            System.out.print(num + " ");
        }
    }
}

归并排序的步骤解析:

  1. mergeSort 主函数:该函数是对外的接口,传入整个数组。它调用递归函数 mergeSort 来处理具体的排序逻辑。

  2. mergeSort 递归函数

    • 这个函数接收一个数组和当前的索引范围(lowhigh)。
    • 它首先计算中间索引 mid,然后递归地对左右两个子数组进行排序。
    • 排序完成后,调用 merge 函数合并两个已排序的子数组。
  3. merge 函数

    • merge 函数用于合并两个有序的子数组(分别位于 lowmidmid+1high)。
    • 使用一个临时数组 temp 来存储合并后的结果。
    • 通过比较两个子数组的元素,将较小的元素逐个放入临时数组。
    • 最后,将临时数组的内容复制回原数组,完成合并。
  4. main 函数:用于测试归并排序。通过打印排序前后的数组,验证归并排序是否正确工作。

归并排序的性能分析:

  • 时间复杂度

    • 最坏时间复杂度O(n log n),无论输入数据如何,归并排序的时间复杂度始终是 O(n log n)。这因为每次递归都会将数组分成两半,而合并操作需要 O(n) 的时间。
    • 平均时间复杂度O(n log n),归并排序的平均情况与最坏情况相同。
    • 最好时间复杂度O(n log n),即使数组已经排序,归并排序的时间复杂度依然是 O(n log n),因为它总是会进行分解和合并。
  • 空间复杂度O(n),归并排序需要额外的空间来存储临时数组。每次合并操作都需要一个新的临时数组来存放合并的结果。

举例说明:

假设我们有数组 [10, 7, 8, 9, 1, 5],归并排序的过程如下:

  1. 第一次递归:将数组分成两部分 [10, 7, 8][9, 1, 5]

  2. 第二次递归:对 [10, 7, 8] 继续分解,得到 [10][7, 8]

    • [7, 8] 排序后变为 [7, 8]
    • 合并 [10][7, 8] 得到 [7, 8, 10]
  3. 第三次递归:对 [9, 1, 5] 继续分解,得到 [9][1, 5]

    • [1, 5] 排序后变为 [1, 5]
    • 合并 [9][1, 5] 得到 [1, 5, 9]
  4. 合并结果:将 [7, 8, 10][1, 5, 9] 合并得到排序后的数组 [1, 5, 7, 8, 9, 10]

总结:

归并排序是一种稳定且高效的排序算法,特别适合于大数据量的排序。它的时间复杂度为 O(n log n),在最坏情况下也能保持较好的性能。不过,它的空间复杂度较高,需要额外的空间来存储临时数组。在实际应用中,归并排序适用于处理大规模数据集的排序。

;