【算法】快速排序及优化

  • 2020 年 3 月 28 日
  • 筆記

一、荷兰国旗问题

在讲快速排序前,我们先来看看荷兰国旗问题。题目如下:

给定一个数组arr,和一个数num,请把  小于num的数放在数组的左边,  等于num的数放在数组的中间,  大于num的数放在数组的 右边。  要求额外空间复杂度O(1),时间复杂度O(N),  例如:  对于数组3,5,4,5,8,1,9,以5进行分割  输出:3 4 1 5 5 9 8 

其实,这就是快排的partition过程,通过三个指针,index,less,more进行的,初始less=左边界-1,more=右边界+1,说明一开始不存在less域和more域: 1)若arr[index] < num,less域增加,即less++,然后index和less位置的数交换后,index++继续指向下一个数 2)若arr[index] > num,more域增加,即more++,然后index和more的数交换后,继续判断交换后arr[index]和num的关系 3)若arr[index] == num,index++继续指向下一个数,不坐任何处理 4)重复以上过程,直至index==more

大致过程图:

代码实现

public static int[] partition(int[] arr, int l, int r, int num) {      int less = l - 1;      int more = r + 1;      int index = l;      // 循环至index到more相等      while (index < more) {        if (arr[index] < num) {          // 扩大less域          swap(arr, ++less, index++);        }else if (arr[index] > num){          // 扩大more域,且交换后,继续判断当前index的数和num的关系          swap(arr, --more, index);        }else {          index++;        }      }      int[] result = {less + 1, more - 1};      // 返回相等区间开始和结束的索引值      return result;    }    public static void swap(int[] arr, int i, int j) {      int tmp = arr[i];          arr[i] = arr[j];          arr[j] = tmp;    }

二、经典快速排序

快排其实是冒泡排序的改进,其算法思路如下: 1)在数组中选取一个数作为基准元素 2)分区,把小于基准的数放左边,大于基准的数放右边 3)递归,对左右两个分区重复以上步骤

在网上找了一张图,如下所示(侵删):

实现代码

  public static void quickSort(int[] arr) {      if (arr == null || arr.length < 2) {        return;      }      quickSort(arr, 0, arr.length - 1);    }      public static void quickSort(int[] arr, int l, int r) {      if (l >= r) {        return;      }      int p = partition(arr, l, r);      quickSort(arr, l, p - 1);      quickSort(arr, p + 1, r);    }      public static int partition(int[] arr, int l, int r) {      // 取左边第一个数为基准      int pivot = arr[l];      int i = l;      int j = r;      while(i != j) {        // 从右边找第一个小于基准的数        while(arr[j] >= pivot && i < j) {          j--;        }        // 从左边找第一个大于基准的数        while(arr[i] <= pivot && i < j) {          i++;        }        if (i < j) {          swap(arr, i, j);        }      }      // 将基准归位      swap(arr, l, j);      // 返回基准的位置      return j;    }

三、改进的快速排序

回顾经典的快速排序,可以优化的地方有两个:

1)基准的选取,若每次取数组的第一个作为基准,则可能出现基准是数组中最小的数,造成了多余的计算。因此,基准的选取可以进行随机选择 2)分区返回的边界,回顾荷兰国旗问题,分区时,可以发现基准可以是中间一片区域,而不是一个数,因此,分区时,返回的不在是基准的位置,而是基准的左边界和右边界,那么下次划分子问题时,就可以减少交换的次数了。

代码实现

public static void quickSort(int[] arr) {      if (arr == null || arr.length < 2) {        return;      }      // quickSort2(arr, 0, arr.length - 1);      quickSort(arr, 0, arr.length - 1);    }    public static void quickSort(int[] arr, int l, int r) {      if (l < r) {        // 从l~r中选一个数,放在r作为基准        swap(arr, l + (int) (Math.random() * (r - l + 1)), r);        int[] p = partition(arr, l, r);        quickSort(arr, l, p[0] - 1);        quickSort(arr, p[1] + 1, r);      }    }      // 分割    public static int[] partition(int[] arr, int l, int r) {      int less = l - 1;      int more = r;      int index = l;      // 循环至index到more相等      while (index < more) {        if (arr[index] < arr[r]) {          // 扩大less域          swap(arr, ++less, index++);        }else if (arr[index] > arr[r]){          // 扩大more域,且交换后,继续判断当前index的数和num的关系          swap(arr, --more, index);        }else {          index++;        }      }      // 将基准归位,基准取的是最右边的数      swap(arr, more, r);      int[] result = {less + 1, more};      // 返回相等区间开始和结束的索引值      return result;    }