动画:一篇文章快速学会快速排序

内容介绍

快速排序简介

快速排序(Quicksort)是对冒泡排序的一种改进。快速排序由C. A. R. Hoare在1960年提出。快速排序算法被列为20世纪十大算法之一,这足以说明的他的做用和重要性。快速排序是程序员必须掌握的一种排序算法。java

希尔排序至关于直接插入排序的升级,它们同属于插入排序类,快速排序其实就是咱们前面认为最慢的冒泡排序的升级,它们都属于交换排序类。它也是经过不断比较和移动交换来实现排序的,只不过它的实现,增大了记录的比较和移动的距离,快速排序会取一个分界值,将比分界值大的记录从前面直接移动到后面,比分界值小的记录从后面直接移动到前面,从而减小了总的比较次数和移动交换次数。程序员

快速排序的思想

快速排序的思想:取一个分界值,经过一趟排序将要排序的数据分割成独立的两部分,其中一部分的全部数据都比分界值小,另一部分的全部数据比分界值大,而后再按此方法对这两部分数据分别进行相同操做,整个排序过程能够递归进行,最终达到整个数据变成有序序列。 算法

快速排序动画演示

快速排序分析

通常没有特殊要求排序算法都是升序排序,小的在前,大的在后。 数组由{5, 3, 1, 9, 7, 2, 8, 6} 这8个无序元素组成。编程

快速排序步骤:数组

  1. 取一个分界值:咱们暂且拿待排序数据的最前一个元素做为分界值(枢轴)。 微信

  2. 分区,low到high之间的元素分红左边小于枢轴,最右边大于枢轴。 dom

  3. 分区后小于枢轴和大于枢轴的两个区域再进行分区,依次类推直到每一个分区数据知足左边小于枢轴,右边大于枢轴,排序完成。 性能

最终结果,以下图: 大数据

快速排序须要解决的两个问题:优化

  1. 分区后还须要分区可使用递归。
  2. 分区时如何让小于枢轴的数据放到枢轴左边,大于枢轴的数据放到枢轴的右边。 使用两个指针(i, j),i是用来找小于枢轴的数据,j是用来找大于枢轴的数据。 i. 循环查找到须要换位置的数据,进行换位置。 ii. 当i索引的数据大于枢轴,这个数据须要换位置,记录i的值,中止查找。 iii. 当j索引的数据小于枢轴,这个数据须要换位置,记录j的值,中止查找。 iv. 若是i > j说明已经找完了,退出循环。 v. 让i和j位置的元素换位置,i++,指针向右移动继续找大于枢轴的数据,j--向左移动继续找小于枢轴的数据。过程以下动画所示:

快速排序代码编写

代码说明:

  1. void quickSort(int[] arr)方法:用于快速排序的方法,参数为须要排序的数组。
  2. void qSort(int[] arr, int low, int high)方法:用于将数组指定范围的数据进行快速排序,此方法不暴露给用户使用。
  3. int partition(int[] arr, int low, int high)方法:快速排序的分区,将low到high之间的元素分红左边小于枢轴,最右边大于枢轴,返回枢轴的位置。
  4. void swap(int[] arr, int start, int end)方法:将arr数组start索引和end索引的元素进行交换位置。

快速排序代码以下:

public class QuickSortTest {
    public static void main(String[] args) {
        int[] arr = new int[] {5, 3, 1, 9, 7, 2, 8, 6};
        quickSort(arr);

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

    public static void quickSort(int[] arr) {
        qSort(arr, 0, arr.length-1);
    }

    // 对arr数组的[low, right]部分进行快速排序
    private static void qSort(int[] arr, int low, int high) {
        if (low >= high) return;
        // 快速排序的分区,将low到high之间的元素分红左边小于枢轴,最右边大于枢轴
        int pivot = partition(arr, low, high);

        // 再次对枢轴左边和右边的数据进行分区。
        qSort(arr, low, pivot - 1);
        qSort(arr,pivot + 1, high);
    }

    // 快速排序的分区,将low到high之间的元素分红左边小于枢轴,最右边大于枢轴,返回枢轴的位置。
    private static int partition(int[] arr, int low, int high) {
        // 将第一个元素做为枢轴
        int v = arr[low];

        int i = low + 1; // arr[low+1, i) <= v; arr(j, high] >= v
        int j = high;

        while (true) {
            // 从左边找到大于枢轴的数据
            while (i <= high && arr[i] < v) {
                i++;
            }

            // 从右边找到小于枢轴的数据
            while (j >= low+1 && arr[j] > v) {
                j--;
            }
            if (i > j) break;

            swap(arr, i, j); // 交换i和j位置的元素
            i++; // 左边的指针向右移动继续找大于枢轴的数据
            j--; // 右边的指针向左移动继续找小于枢轴的数据
        }

        // 交换枢轴到j索引,保证枢轴左边的元素小于枢轴,枢轴右边元素大于枢轴。
        swap(arr, low, j);
        return j;
    }

    // 数组两个元素交换
    public static void swap(int[] arr, int start, int end) {
        if (start == end)
            return;

        int temp = arr[start];
        arr[start] = arr[end];
        arr[end] = temp;
    }
}

快速排序代码优化1

优化枢轴的选取

咱们知道快速会不断对数据进行分区,选定一个枢轴,将小于枢轴的数据放到左边,大于枢轴的数据放到右边。

前面咱们在对数据进行分区时,都是以数组最前面一个元素做为枢轴,枢轴的选取不够合理。这样会存在一个问题,当数据自己近乎有序时好比数据为:{1, 2, 3, 5, 6, 7, 9, 8},分区时选择最左边的数据做为枢轴,刚好是数组最小或最大数据,致使分区时,数据都在数轴一侧会致使快速排序退化为一个O(n2)的算法。

如何选取枢轴才不会让近乎有序的数据排序退化成O(n^2)呢,咱们能够看到缘由是咱们一直选取数组最前面的一个数据做为枢轴,所以咱们能够随机选取一个元素做为数轴,这样,每次都选取到最大或最小的几率就会很是低。改进后的代码以下:

public class QuickSortTest2 {
    public static void main(String[] args) {
        int[] arr = new int[] {5, 3, 1, 9, 7, 2, 8, 6};
        quickSort(arr);

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

    public static void quickSort(int[] arr) {
        qSort(arr, 0, arr.length-1);
    }

    // 对arr数组的[low, right]部分进行快速排序
    public static void qSort(int[] arr, int low, int high) {
        if (low >= high) return;
        // 快速排序的分区,将low到high之间的元素分红左边小于枢轴,最右边大于枢轴
        int pivot = partition2(arr, low, high);

        // 再次对枢轴左边和右边的数据进行分区。
        qSort(arr, low, pivot - 1);
        qSort(arr,pivot + 1, high);
    }

    // 数组两个元素交换
    public static void swap(int[] arr, int start, int end) {
        if (start == end)
            return;

        int temp = arr[start];
        arr[start] = arr[end];
        arr[end] = temp;
    }

    // 快速排序的分区,将low到high之间的元素分红左边小于枢轴,最右边大于枢轴,返回枢轴的位置。
    private static int partition2(int[] arr, int low, int high) {
        // 将第一个元素做为枢轴,若是数组是近乎有序的数组,那么每次使用第一个元素拆分,会让拆分倾斜到一边,很是的不平衡.
        // 修改为随机选取一个元素做为数轴
        Random ran = new Random();
        // 获得随机的索引
        int rIndex = ran.nextInt(10000000) % (high - low + 1) + low;
        // 拿这个随机索引的数据和最前面的数据交换,这个随机的数据做为数轴
        swap(arr, low, rIndex);

        // 将第一个元素做为枢轴
        int v = arr[low];

        int i = low + 1; // arr[low+1, i) <= v; arr(j, high] >= v
        int j = high;

        while (true) {
            // 从左边找到大于枢轴的数据
            while (i <= high && arr[i] < v) {
                i++;
            }

            // 从右边找到小于枢轴的数据
            while (j >= low+1 && arr[j] > v) {
                j--;
            }
            if (i > j) break;

            swap(arr, i, j); // 交换i和j位置的元素
            i++; // 左边的指针向右移动继续找大于枢轴的数据
            j--; // 右边的指针向左移动继续找小于枢轴的数据
        }

        // 交换枢轴到j索引,保证枢轴左边的元素小于枢轴,枢轴右边元素大于枢轴。
        swap(arr, low, j);
        return j;
    }
}

快速排序代码优化2

小数据量使用插入排序

如今咱们的快速排序是一直分区,直到分区中的每一个元素都有序,咱们知道插入排序在数据量小时效率相对较高,当元素数量较少时,咱们可使用插入排序来替换继续分区,从而提升插入排序的效率,优化后代码以下:

public class QuickSortTest3 {
    public static void main(String[] args) {
        int[] arr = new int[] {5, 3, 1, 9, 7, 2, 8, 6};
        quickSort(arr);

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

    public static void quickSort(int[] arr) {
        qSort(arr, 0, arr.length-1);
    }

    // 对arr数组的[low, right]部分进行快速排序
    public static void qSort(int[] arr, int low, int high) {
        if (low >= high) return;

        if (high - low <= 15) {
            insertionSort(arr, low, high);
            return;
        }

        // 快速排序的分区,将low到high之间的元素分红左边小于枢轴,最右边大于枢轴
        int pivot = partition2(arr, low, high);

        // 再次对枢轴左边和右边的数据进行分区。
        qSort(arr, low, pivot - 1);
        qSort(arr,pivot + 1, high);
    }

    // 对数组指定索引范围的元素使用插入排序
    public static void insertionSort(int[] arr, int low, int high) {
        for (int i = low + 1; i <= high; i++) {
            int e = arr[i]; // 获得当前这个要插入的元素
            int j;
            for (j = i; j > low && arr[j-1] > e; j--) {
                arr[j] = arr[j-1];
            }
            arr[j] = e;
        }
    }

    // 快速排序的分区,将low到high之间的元素分红左边小于枢轴,最右边大于枢轴,返回枢轴的位置。
    private static int partition2(int[] arr, int low, int high) {
        // 将第一个元素做为枢轴,若是数组是近乎有序的数组,那么每次使用第一个元素拆分,会让拆分倾斜到一边,很是的不平衡.
        // 修改为随机选取一个元素做为数轴
        Random ran = new Random();
        // 获得随机的索引
        int rIndex = ran.nextInt(10000000) % (high - low + 1) + low;
        // 拿这个随机索引的数据和最前面的数据交换,这个随机的数据做为数轴
        swap(arr, low, rIndex);

        // 将第一个元素做为枢轴
        int v = arr[low];

        int i = low + 1; // arr[low+1, i) <= v; arr(j, high] >= v
        int j = high;

        while (true) {
            // 从左边找到大于枢轴的数据
            while (i <= high && arr[i] < v) {
                i++;
            }

            // 从右边找到小于枢轴的数据
            while (j >= low+1 && arr[j] > v) {
                j--;
            }
            if (i > j) break;

            swap(arr, i, j); // 交换i和j位置的元素
            i++; // 左边的指针向右移动继续找大于枢轴的数据
            j--; // 右边的指针向左移动继续找小于枢轴的数据
        }

        // 交换枢轴到j索引,保证枢轴左边的元素小于枢轴,枢轴右边元素大于枢轴。
        swap(arr, low, j);
        return j;
    }

    // 数组两个元素交换
    public static void swap(int[] arr, int start, int end) {
        if (start == end)
            return;

        int temp = arr[start];
        arr[start] = arr[end];
        arr[end] = temp;
    }
}

快速排序代码优化3

3路快速排序

前面咱们在进行分区时,大量和枢轴重复的数据还会进入下一次排序。代码以下:

private static int partition2(int[] arr, int low, int high) {
    ...
    while (true) {
        // 从左边找到大于枢轴的数据
        while (i <= high && arr[i] < v) {
            i++;
        }
        // 从右边找到小于枢轴的数据
        while (j >= low+1 && arr[j] > v) {
            j--;
        }
        if (i > j) break;
        swap(arr, i, j); // 交换i和j位置的元素
        i++; // 左边的指针向右移动继续找大于枢轴的数据
        j--; // 右边的指针向左移动继续找小于枢轴的数据
    }
    ...
}

进行一次分区后,大量和枢轴重复的数据还会进入下一次排序,浪费性能,和枢轴相同的数据不用再进入下次分区。效果以下:

所以咱们在进行分区时,能够将数据分红3个区域,小于枢轴的数据,等于数轴的数据,大于枢轴的数据,这样处理的好处是等于枢轴的数据不会进入下一次分区,因此在待排序数据中出现大量重复数据时能够提升效率。

优化后代码:

public class QuickSortTest4 {
    public static void main(String[] args) {
        int[] arr = new int[] {5, 3, 1, 9, 7, 2, 8, 6};
        quickSort3Ways(arr);

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

    public static void quickSort3Ways(int[] arr) {
        qSort3Ways(arr, 0, arr.length-1);
    }

    // 对arr数组的[low, right]部分进行快速排序
    public static void qSort3Ways(int[] arr, int low, int high) {
        if (low >= high) return;

        if (high - low <= 15) {
            insertionSort(arr, low, high);
            return;
        }

        int pivot = partition3Ways(arr, low, high);
        qSort3Ways(arr, low, pivot - 1);
        qSort3Ways(arr,pivot + 1, high);
    }

    private static int partition3Ways(int[] arr, int low, int high) {
        // 将第一个元素做为枢轴,若是数组是近乎有序的数组,那么每次使用第一个元素拆分,会让拆分倾斜到一边,很是的不平衡.
        // 修改为随机选取一个数字进行拆分
        Random ran = new Random();
        int rIndex = ran.nextInt(10000000) % (high - low + 1) + low;
        swap(arr, low, rIndex);
        int v = arr[low];

        int lt = low;     // arr[l+1...lt] < v
        int gt = high + 1; // arr[gt...r] > v
        int i = low+1;    // arr[lt+1...i) == v

        while (i < gt) {
            if (arr[i] < v) {
                swap(arr, i, lt+1);
                i++;
                lt++;
            } else if (arr[i] > v) {
                swap(arr, i, gt-1);
                gt--;
            } else {
                i++;
            }
        }
        swap(arr, low, lt);
        return lt;
    }

    // 对数组指定索引范围的元素使用插入排序
    public static void insertionSort(int[] arr, int low, int high) {
        for (int i = low + 1; i <= high; i++) {
            int e = arr[i]; // 获得当前这个要插入的元素
            int j;
            for (j = i; j > low && arr[j-1] > e; j--) {
                arr[j] = arr[j-1];
            }
            arr[j] = e;
        }
    }

    // 数组两个元素交换
    public static void swap(int[] arr, int start, int end) {
        if (start == end)
            return;

        int temp = arr[start];
        arr[start] = arr[end];
        arr[end] = temp;
    }
}

快速排序代码优化4

减小递归次数

咱们知道,递归对性能有必定影响,上面的qSort3Ways方法内部先进行分区,而后进行两次递归。若是待排序的序列划分极端不平衡,递归深度将趋近于n,而不是平衡时的log2n, 除了分区次数变多,影响排序效率以外。栈的大小是颇有限的,每次递归调用都会耗费必定的栈空间,所以减小递归,能够提升性能,而且防止栈空间不足而致使的栈溢出问题。 减小递归次数后的代码以下:

public class QuickSortTest5 {
    public static void main(String[] args) {
        int[] arr = new int[] {5, 3, 1, 9, 7, 2, 8, 6};
        quickSort3Ways(arr);

        System.out.println("排序后:" + Arrays.toString(arr));
        System.out.println("我被递归了:" + count);
    }

    public static void quickSort3Ways(int[] arr) {
        qSort3Ways(arr, 0, arr.length-1);
    }

    private static int count;
    // 对arr数组的[low, right]部分进行快速排序
    public static void qSort3Ways(int[] arr, int low, int high) {
        if (low >= high) return;
        count++;
        if (high - low <= 15) {
            insertionSort(arr, low, high);
        } else {
            while (low < high) {
                int pivot = partition3Ways(arr, low, high);
                qSort3Ways(arr, low, pivot - 1);
                low = pivot + 1; // 循环会对右边的区域进行分区,而不是递归再对右边进行分区
            }
        }
    }

    private static int partition3Ways(int[] arr, int low, int high) {
        // 将第一个元素做为枢轴,若是数组是近乎有序的数组,那么每次使用第一个元素拆分,会让拆分倾斜到一边,很是的不平衡.
        // 修改为随机选取一个数字进行拆分
        Random ran = new Random();
        int rIndex = ran.nextInt(10000000) % (high - low + 1) + low;
        swap(arr, low, rIndex);
        int v = arr[low];

        int lt = low;     // arr[l+1...lt] < v
        int gt = high + 1; // arr[gt...r] > v
        int i = low+1;    // arr[lt+1...i) == v

        while (i < gt) {
            if (arr[i] < v) {
                swap(arr, i, lt+1);
                i++;
                lt++;
            } else if (arr[i] > v) {
                swap(arr, i, gt-1);
                gt--;
            } else {
                i++;
            }
        }
        swap(arr, low, lt);
        return lt;
    }

    // 对数组指定索引范围的元素使用插入排序
    public static void insertionSort(int[] arr, int low, int high) {
        for (int i = low + 1; i <= high; i++) {
            int e = arr[i]; // 获得当前这个要插入的元素
            int j;
            for (j = i; j > low && arr[j-1] > e; j--) {
                arr[j] = arr[j-1];
            }
            arr[j] = e;
        }
    }

    // 数组两个元素交换
    public static void swap(int[] arr, int start, int end) {
        if (start == end)
            return;

        int temp = arr[start];
        arr[start] = arr[end];
        arr[end] = temp;
    }
}

总结

  1. 快速排序其实就是咱们前面认为最慢的冒泡排序的升级,它们都属于交换排序类。
  2. 快速排序的思想:取一个分界值,经过一趟排序将要排序的数据分割成独立的两部分,其中一部分的全部数据都比分界值小,另一部分的全部数据比分界值大,而后再按此方法对这两部分数据分别进行相同操做,整个排序过程能够递归进行,最终达到整个数据变成有序序列。
  3. 快速排序代码优化1:优化枢轴的选取。
  4. 快速排序代码优化2:小数据量使用插入排序。
  5. 快速排序代码优化3: 3路快速排序。
  6. 快速排序代码优化4:减小递归次数。

快速排序算法被列为20世纪十大算法之一,通过屡次的优化后,在总体性能上,依然是排序算法王者,快速排序是程序员必须掌握的一种排序算法。


原创文章和动画制做真心不易,您的点赞就是最大的支持! 想了解更多文章请关注微信公众号:表哥动画学编程

相关文章
相关标签/搜索