算法导论-第7章-快速排序

7.1 快速排序的描述

对一个典型的子数组\(A[p..r]\)进行快速排序的三步分治过程:

  • 分解:数组\(A[p..r]\)被划分为两个(可能为空)的子数组\(A[p..q-1]\)\(A[q+1..r]\),使得\(A[p..q-1]\)中的每一个元素都小于等于\(A[q]\),而\(A[q+1..r]\)中的每个元素都大于等于\(A[q]\)。返回下标\(q\)
  • 解决:通过递归,对子数组\(A[p..q-1]\)\(A[q+1..r]\)调用快速排序。
  • 合并:数组\(A[p..r]\)已经有序。

下面的程序实现快速排序:

下图显示了PARTITION(A, p, r)的操作过程:选择\(x=A[r]\)作为枢轴(pivot)(不一定非要选择数组最后一个元素作为枢轴,也可以选择其他元素),并围绕它来划分子数组\(A[p..r]\)

PARTITION(A, p, r)的核心思想:取数组的最后一个元素为枢轴,使用指针\(j\)从左向右遍历,遇到比枢轴小的元素,移动指针\(i\)。这就形成了在从数组开头到指针\(j\)的范围内,从数组开头到指针\(i\)为比枢轴小的元素,从指针\(i+1\)到指针\(j-1\)为比枢轴大的元素,直到遍历\(A.length-1\)个元素。最后,交换指针\(i+1\)指向的元素和数组最后一个元素即可。

PARTITION(A, p, r)在操作过程中将待排序的子数组划分为以下几个部分:

  • \(A[p..i]\):已经遍历的比枢轴元素小的元素
  • \(A[i+1..j-1]\):已经遍历的比枢轴元素大的元素
  • \(A[j..r-1]\):将要遍历的元素
  • \(A[r]\):枢轴元素


快速排序实验:读取data.txt文件,文件格式如下:第一行为数组长度,第二行为数组(int类型)的内容,将结果数组的数据写在一行内,每个数组中间以空格隔开,输出为sorted.txt。

代码实现:

import java.io.BufferedWriter;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.List;

public class QuickSort {

    public static void swap(int[] arr, int i, int j) {
        int temp = arr[i];
        arr[i] = arr[j];
        arr[j] = temp;
    }

    public static void quickSort(int[] arr, int left, int right) {
        if (left < right) {
            int pivotIndex = partition(arr, left, right);
            quickSort(arr, left, pivotIndex - 1);
            quickSort(arr, pivotIndex + 1, right);
        }
    }

    public static int partition(int[] arr, int left, int right) {
        int pivot = arr[right];
        int i = left - 1;
        for (int j = left; j < right; j++) {
            if (arr[j] < pivot) {
                i++;
                swap(arr, i, j);
            }
        }
        swap(arr, i + 1, right);
        return i + 1;
    }


    public static void main(String[] args) throws IOException {
        /*
          1、读取data.txt文件中的数据(第一行为数组长度, 第二行为数组(int类型)的内容)
         */
        String path = "D:/Projects/IDEAProjects/algorithms/src/main/java/ch07/data.txt";
        List<String> readAllLines = Files.readAllLines(Paths.get(path));
        int length = Integer.parseInt(readAllLines.get(0));
        String[] split = readAllLines.get(1).split("\\s+");
        int[] array = new int[length];
        for (int i = 0; i < length; i++) {
            array[i] = Integer.parseInt(split[i]);
        }

        /*
          2、计算算法的耗时
         */
        long start = System.currentTimeMillis();
        quickSort(array, 0, length - 1);
        long end = System.currentTimeMillis();

        System.out.println("算法耗时: " + (end - start) + " ms");


        /*
          3、将排序后的数据写入结果文件result.txt
         */
        String pathResult = "D:/Projects/IDEAProjects/algorithms/src/main/java/ch07/sorted.txt";
        File file = new File(pathResult);
        if (!file.exists()) {
            file.createNewFile();
        }

        FileWriter fileWriter = new FileWriter(file);
        BufferedWriter bufferedWriter = new BufferedWriter(fileWriter);

        StringBuilder stringBuilder = new StringBuilder();
        for (int i = 0; i < length; i++) {
            stringBuilder.append(array[i]).append(" ");
        }
        bufferedWriter.write(stringBuilder.toString());
        bufferedWriter.close();
        //System.out.println(Arrays.toString(array));
    }
}

7.2 快速排序的优化

优化思路:

  1. 基准的选择:快速排序的运行时间与划分是否对称有关。最坏情况下,每次划分过程产生两个区域分别包含\(n-1\)个元素和\(1\)个元素,其时间复杂度会达到\(O(n^2)\)。在最好的情况下,每次划分所取的基准都恰好是中值,即每次划分都产生两个大小为\(n/2\)的区域。此时,快排的时间复杂度为\(O(n \log n)\)

    所以基准的选择对快排而言至关重要。快排中基准的选择方式主要有以下三种:

    1. 固定基准
    2. 随机基准
    3. 三数取中
  2. 当输入数据已经“几乎有序”时,使用插入排序速度很快。我们可以利用这一特点来提高快速排序的速度。当对一个长度小于 \(k\) 的子数组调用快速排序时,让她不做任何排序就返回。上层的快速排序调用返回后,对整个数组运行插入排序来完成排序过程。

  3. (可选)聚集元素

    思想:在一次分割结束后,将与本次基准相等的元素聚集在一起,再分割时,不再对聚集过的元素进行分割。

    1. 在划分过程中将与基准值相等的元素放入数组两端,
    2. 划分结束后,再将两端的元素移到基准值周围。
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.List;

public class QuickSortPro {
    private final static int THRESHOLD = 8; // 插入排序阈值

    /**
     * 交换数组中两个索引位置的元素
     * @param arr
     * @param i
     * @param j
     */
    public static void swap(int[] arr, int i, int j) {
        int temp = arr[i];
        arr[i] = arr[j];
        arr[j] = temp;
    }

    /**
     * 插入排序
     * @param A
     * @param left
     * @param right
     */
    public static void insertionSort(int[] A, int left, int right) {
        for (int j = left + 1; j <= right; j++) {
            int key = A[j];
            int i = j - 1;
            while (i >= left && A[i] > key) {
                A[i + 1] = A[i];
                i--;
            }
            A[i + 1] = key;
        }
    }

    /**
     * 快速排序优化
     * @param arr
     * @param left
     * @param right
     */
    public static void quickSortPro(int[] arr, int left, int right) {
        if (right - left + 1 <= THRESHOLD) {
            insertionSort(arr, left, right);
        }
        if (left < right) {
            int pivotIndex = randomPartition(arr, left, right);
            //int pivotIndex = partition(arr, left, right);

            quickSortPro(arr, left, pivotIndex - 1);
            quickSortPro(arr, pivotIndex + 1, right);
        }
    }

    /**
     * 三数取中,将中间大的元素交换到枢轴的位置
     * @param arr
     * @param left
     * @param right
     * @return
     */
    public static int randomPartition(int[] arr, int left, int right) {
        int mid = left + (right - left) / 2;
        if (arr[left] > arr[mid]) {
            swap(arr, left, mid);
        }
        if (arr[mid] > arr[right]) {
            swap(arr, mid, right);
        }
        if (arr[mid] < arr[left]) {
            swap(arr, left, mid);
        }

        swap(arr, mid, right);
        return partition(arr, left, right);
    }

    /**
     * 选取的枢轴为数组中最后一个元素
     * @param arr
     * @param left
     * @param right
     * @return
     */
    public static int partition(int[] arr, int left, int right) {
        int pivot = arr[right];
        int i = left - 1;
        for (int j = left; j < right; j++) {
            if (arr[j] < pivot) {
                i++;
                swap(arr, i, j);
            }
        }
        swap(arr, i + 1, right);
        return i + 1;
    }


    public static void main(String[] args) throws IOException {
        /*
          1、读取data.txt文件中的数据(第一行为数组长度, 第二行为数组(int类型)的内容)
         */
        String path = "D:/Projects/IDEAProjects/algorithms/src/main/java/ch07/data.txt";
        List<String> readAllLines = Files.readAllLines(Paths.get(path));
        int length = Integer.parseInt(readAllLines.get(0));
        String[] split = readAllLines.get(1).split("\\s+");
        int[] array = new int[length];
        for (int i = 0; i < length; i++) {
            array[i] = Integer.parseInt(split[i]);
        }

        /*
          2、计算算法的耗时
         */
        long start = System.currentTimeMillis();
        quickSortPro(array, 0, length - 1);
        long end = System.currentTimeMillis();

        System.out.println("算法耗时: " + (end - start) + " ms");


        /*
          3、将排序后的数据写入结果文件result.txt
         */
        String pathResult = "D:/Projects/IDEAProjects/algorithms/src/main/java/ch07/sorted-pro.txt";
        File file = new File(pathResult);
        if (!file.exists()) {
            file.createNewFile();
        }

        FileWriter fileWriter = new FileWriter(file);
        BufferedWriter bufferedWriter = new BufferedWriter(fileWriter);

        StringBuilder stringBuilder = new StringBuilder();
        for (int i = 0; i < length; i++) {
            stringBuilder.append(array[i]).append(" ");
        }
        bufferedWriter.write(stringBuilder.toString());
        bufferedWriter.close();
        //System.out.println(Arrays.toString(array));
    }
}

7.3 常见排序算法的运行时间

算法 最坏运行时间 平均情况/期望运行时间
插入排序 \(\Theta(n^2)\) \(\Theta(n^2)\)
归并排序 \(\Theta(n \log n)\) \(\Theta(n \log n)\)
堆排序 \(\Omicron(n \log n)\) --------
快速排序 \(\Theta(n^2)\) \(\Theta(n \log n)\)(期望)
计数排序 \(\Theta(k+n)\) \(\Theta(k+n)\)
基数排序 \(\Theta(d(n+k))\) \(\Theta(d(n+k))\)
桶排序 \(\Theta(n^2)\) \(\Theta(n)\)(平均情况)
posted @ 2023-06-29 21:39  gengduc  阅读(40)  评论(0编辑  收藏  举报