排序-2-快速排序

494 阅读4分钟

同冒泡排序一样,快速排序也属于交换排序,通过元素之间的比较和交换位置来达到排序的目的

不同的是,冒泡排序在每一轮中只把1个元素冒泡到数列的一端,而快速排序则在每一轮挑选一个基准元素,并让其它比它大的元素移动的数列的一边,比它小的元素移动到数列的另一边,从而把数列拆解成两个部分

这种思路叫做分治法

流程:

每一轮的比较和交换都需要把数组全部元素都遍历一遍,时间复杂度是O(n),需要遍历logn轮,所以时间复杂度为O(nlogn)

基准元素的选择

最简单的方式是选择数列的第一个元素

这种选择在大多数情况下是没问题的,但是假如有一个原本逆序的数列,期望排序称为顺序的数列,则会出现时间复杂度退化称为O(n2)的情况

避免这种情况的做法是随机选取一个元素作为基准元素,并且让基准元素和列首元素交换位置

但是即使随机选择元素,也有极小的几率选择到数列的最大值或者最小值,所以虽然快速排序的平均时间复杂度是O(nlogn),但是最坏的情况下时间复杂度是O(n2)

元素交换

设定基准元素之后,就是要把其他元素中小于基准元素的都交换到基准元素的一边,大于基准元素的都交换到基准元素的另一边,

具体实现有两种方法:

1. 双边循环法

· 2. 单边循环法

双边循环法

详细过程如下:

原始数组如下,要求对其从小到大排序

1. 首先,选择基准元素 pivot,并且设置两个指针 left 和 right,指向数列的最左和最右两个元素

2. 进行第一次循环,从 right 指针开始,让指针指向的元素和基准元素进行比较,如果大于 pivot,则指针向左移动,如果小于等于 pivot 则 right 指针停止移动,切换 left 指针

在当前数列中,1 < 4 所以right指针直接停止移动,

切换 left 指针,让指针指向的元素和基准元素进行比较,如果小于或等于 pivot,则指针向右移动,如果大于 pivot,则 left 指针停止移动

由于left指针开始指向基准元素,判断肯定相等,所以 left 右移一位,

由于 7 > 4 则 left 指针停下,这时让 left 和 right 指针所指向的元素进行交换

3. 接下来重新切换到 right 指针 进行第二次循环,后续步骤如下:

代码如下

      /**
       * @param {Array<number>} arr 待交换的数组
       * @param {number} startIndex 起始下标
       * @param {number} endIndex 结束下标
       * @returns {number} 返回基准元素位置
       * @description 分治 双边循环法
       */
      function partition(arr: Array<number>, startIndex: number, endIndex: number):number {
          // 取第一个位置的元素作为基准元素
          const pivot = arr[startIndex];
          // left 指针
          let left = startIndex;
          // right 指针
          let right = endIndex;
      
      
          while(left != right) {
              // 控制right指针左移
              while(left < right && arr[right] > pivot) {
                  right--
              }
              // 控制left指针右移
              while(left < right && arr[left] <= pivot) {
                  left++
              }
              // 如果指针不重合 交换left right 指针所指元素
              if(left < right) {
                  let p = arr[left];
                  arr[left] = arr[right];
                  arr[right] = p;
              }
          }
          // pivot 和重合点交换
          arr[startIndex] = arr[left];
          arr[left] = pivot;
          /* 
              这里有个问题思索了很久:怎么保证交换的left元素 一定是小于 或者 大于 基准元素
              细想下来:如上代码  left 或者 right 其中一个停止,另一个 移动过来重合
                      如果 left => right 则 right一定为 小于= pivot 的元素
                      如果 left <= right 则 一定为 小于= pivot的元素
          */
          return left
      }
      
      
      
      function quickSort(arr: Array<number>, startIndex: number, endIndex: number) {
          if(startIndex >= endIndex) {
              return
          }
          // 得到基准元素的位置
          let pivotIndex = partition(arr, startIndex, endIndex);
          // 根据基准元素 分为两部分递归排序
          quickSort(arr, startIndex, pivotIndex - 1);
          quickSort(arr, pivotIndex + 1, endIndex);
      }
      
      
      
      function main() {
          let arr = [4, 2, 6, 2, 2, 2, 2, 8];
          quickSort(arr, 0, arr.length - 1 );
          console.log(arr)
      }
      main()
      
      
      

单边循环法

给出原始数组,要求对其从小到大进行排序:

选的基准元素 pivot,同时设置一个 mark 指针指向数列起始位置,这个 mark 指针代表 小于基准元素的区域边界

接下来,从基准元素的下一个位置开始遍历数组,

如果遍历到的元素大于基准元素,就继续往后遍历

如果遍历到的元素小于基准元素,则需要做两件事情:

第一、把 mark 指针右移 1 位,因为小于 pivot 的区域边界增大了 1

第二、让最新遍历到的元素和 mark 指针所在位置的元素交换位置,因为最新遍历的元素属于小于 pivot 的区域

首先遍历到元素 7,7 > 4,所以继续遍历

接下来遍历到元素 3,3 < 4,所以 mark 指针右移 1 位

随后,让元素 3 和 mark 指针所在位置的元素交换,因为 3 归属小于 pivot 区域

后续步骤如下:

代码如下:

    /**
     * @param {Array<number>} arr 待交换的数组
     * @param {number} startIndex 起始位置
     * @param {number} endIndex 结束位置
     * @description 分治 单边循环法
     */
    function partition(arr: Array<number>, startIndex: number, endIndex: number):number {
        // 拿到基准元素
        let pivot = arr[startIndex];
        // 边界
        let mark = startIndex;
    
    
        for (let index = startIndex; index <=  endIndex; index++) {
            if(arr[index] < pivot) {
                mark++;
                let p =arr[mark];
                arr[mark] = arr[index];
                arr[index] = p 
            }        
        }
    
    
        arr[startIndex] = arr[mark];
        arr[mark] = pivot;
        return mark
    }
    
    
    function quickSort(arr: Array<number>, startIndex: number, endIndex: number){
        if(startIndex >= endIndex) {
            return 
        }
    
    
        const pivotIndex = partition(arr, startIndex, endIndex);
    
    
        quickSort(arr, startIndex, pivotIndex - 1);
        quickSort(arr, pivotIndex + 1, endIndex)
    }
    
    
    function main() {
        let arr = [4, 4, 6, 5, 3, 2, 8, 1];
        quickSort(arr, 0, arr.length - 1);
        console.log(arr)
    }
    
    
    main();

非递归实现

    /**
     * @param {Array<number>} arr 待排序的数组
     * @param {number} startIndex  起始位置
     * @param {number} endIndex  结束位置
     * @returns 基准元素下标
     * @description 分治 单边循环法
     */
    function partition(arr: Array<number>, startIndex: number, endIndex: number) {
        // 基准元素
        let pivot = arr[startIndex]
        // 边界
        let mark = startIndex
    
    
        for (let index = startIndex; index <= endIndex; index++) {
            if(arr[index] < pivot) {
                mark++;
                let p = arr[mark];
                arr[mark] = arr[index];
                arr[index] = p
            }        
        };
    
    
        arr[startIndex] = arr[mark];
        arr[mark] = pivot;
        return mark
    }
    
    
    interface StackItem {
        startIndex: number;
        endIndex: number;
    }
    
    
    function quickSort(arr: Array<number>, startIndex: number, endIndex: number) {
        // 用一个栈来代替递归的调用栈
        let quickSortStack:Array<StackItem> = [];
        // 起止下标
        let obj:StackItem = {
            startIndex,
            endIndex
        };
        quickSortStack.push(obj);
        // 当栈为空时结束
        while(quickSortStack.length) {
            let item = quickSortStack.pop() as StackItem;
            let { startIndex, endIndex } = item
            let pivotIndex = partition(arr, startIndex, endIndex);
            // 如果一边有两个 或者两个以上的元素 则继续入栈
            if(startIndex < pivotIndex - 1) {
                quickSortStack.push({
                    startIndex,
                    endIndex: pivotIndex - 1
                })
            }
            if(endIndex > pivotIndex + 1) {
                quickSortStack.push({
                    startIndex : pivotIndex + 1,
                    endIndex: endIndex
                })
            }
        }
    }
    
    
    function main() {
        let arr = [4, 4, 6, 5, 3, 2, 8, 1];
        quickSort(arr, 0, arr.length - 1);
        console.log(arr)
    }

摘要总结自: 漫画算法 小灰的算法之旅