排序算法

101 阅读3分钟

一张图概括

image.png

核心思想

在一个排序算法中,需要通过循环对元素的位置进行调整,因此 每次循环时,算法所完成的任务可以抽象化为将数据从状态A转移到状态B,同时状态B也是下一次转移的起始状态。

因此,抓住一个排序时的循环不变式,保持这个条件交换或者移动其他元素,就能证明一个排序算法的正确性 参考算法导论的定义,一个循环不变式有以下几个性质

  1. 循环的第一次迭代前位置
  2. 如果循环的某次迭代之前为真,那么下次迭代前依然为真
  3. 在循环终止时,不变式为我们提供一个有用的性质,该性质有助于证明算法是正确的。

选择排序

  1. 先在未排序的序列中找到最小(大)元素,存放在排序序列的起始位置
  2. 再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。
  3. 重复步骤2直到排序完成

JS实现

function selectionSort(arr){
    let len = arr.length;

    let minIndex,temp // 用于存储未排序的位置和交换用的之

    // 默认第一个元素是排好序的,遍历len-1个另外的
    for(let i=0;i<len-1;i++){
        minIndex = i; // 标记当前未排序序列的第一个值
        for(let j = i+1;j<len;j++){
            // 记录未排序序列中最小元素的位置
            if(arr[j]<arr[minIndex]){
                minIndex = j;
            }
        }   
        // 
        temp = arr[j];
        arr[i] = arr[minIndex];
        arr[minIndex] = temp;

    }
    return arr;
}

冒泡排序

反复交换没有按次序排列的元素,每一次冒泡都可以将一个最大或者最小的元素放在最前或者最后

function bubbleSort(arr) {  
    var len = arr.length;  
    for (var i = 0; i < len - 1; i++) {  
        for (var j = 0; j < len - 1 - i; j++) {  
            if (arr[j] > arr[j+1]) {        // 相邻元素两两对比  
                var temp = arr[j+1];        // 元素交换  
                arr[j+1] = arr[j];  
                arr[j] = temp;  
            }  
        }  
    }  
    return arr;  
}

优化冒泡排序

用一个flag标记是否循环

    public int[] sort(int[] sourceArray) throws Exception {  
        // 对 arr 进行拷贝,不改变参数内容  
        int[] arr = Arrays.copyOf(sourceArray, sourceArray.length);  
  
        for (int i = 1; i < arr.length; i++) {  
            // 设定一个标记,若为true,则表示此次循环没有进行交换,也就是待排序列已经有序,排序已经完成。  
            boolean flag = true;  
  
            for (int j = 0; j < arr.length - i; j++) {  
                if (arr[j] > arr[j + 1]) {  
                    int tmp = arr[j];  
                    arr[j] = arr[j + 1];  
                    arr[j + 1] = tmp;  
  
                    flag = false;  
                }  
            }  
  
            if (flag) {  
                break;  
            }  
        }  
        return arr;  
    }  
}

归并排序

核心思想:划分子序列直到不能划分为止,对每个子序列进行排序后合并

分治: 将数组换分为两个规模为n/2的子数组 递归: 递归地对子数组进行排序 合并: 递归地合并两个子数字

有两种实现的思路

  1. 自上而下进行递归
  2. 自下而上实现迭代

实现

  1. 申请空间 大小为两个已经排序的序列大小之和,存放合并后的序列
  2. 使用两个指针记录这两个已经排序的序列的起始位置
  3. 比较两个指针指向的元素,选择这两个中较小的一个放入合并空间中,指针后移
  4. 重复步骤3直至其中一个指针到末尾
  5. 处理剩余元素

JS实现


function mergeSort(arr) {
    let len = arr.length;
    if (len < 2) {
        return arr;
    }

    // 分治
    let middle = Math.floor(len / 2);
    // 子问题求解
    let left = arr.slice(0, middle);
    let right = arr.slice(middle);

    function merge(left, right) {
        // 开辟一个和原始序列一样大小的新空间
        var result = [];

        // 当两个指针均没有移动到最后
        while (left.length && right.length) {
            if (left[0] < right[0]) {

                result.push(left.shift());
            } else {
                result.push(right.shift())
            }
        }

        // 处理剩余
        while (left.length)
            result.push(left.shift());

        while (right.length)
            result.push(right.shift());

        return result;
    }
}

快速排序

同样基于递归分治思路

  1. 分解:将数组划分为两个子数组(可以为空),挑选其中一个元素作为基准(pivot),对序列进行重新排序,所有被基准值小的元素放到基准前,比基准值大的放在基准后,实现分区。
  2. 子问题解决: 递归地对两个子数组进行排序
  3. 合并:原地排序无须合并

时间复杂度

  1. 最坏情况:每次划分的数组都是n-1个元素和0个元素,划分操作的时间复杂度为O(n)O(n) 整体的时间复杂度O(n2)O(n^2)
  2. 最好情况: 划分时均分 O(logn)O(log n)
  3. 平均情况O(logn)O(log n) 常数比例的划分方法产生深度为O(logn)O(logn) 递归树 每层代价为O(n)O(n)

JS实现

function quickSort (arr,left,right){
    let len = arr.length;
    let partitionIndex; // 分割的index
    // 增加如果没传入的判断
    let left = typeof left == 'number'?0:left;
    let right = typeof right == 'number'?len-1:right;

    if(left<right){
        partitionIndex = partition(arr,left,right);
        // 分治排序
        quickSort(arr,left,partitionIndex-1); // 排序基准左侧的子序列
        quickSort(arr,partitionIndex+1,right);
    }

    return arr;
  }

function partition(arr,left,right){
    // 选最左侧点作为排序基准点
    let pivot = left;
    let index = pivot+1;// 用于遍历基准意外的元素
    for(let i=index;i<right;i++){
        // 如果遍历到的元素小于基准值则将其与排序序列的最左侧元素互换,说明i之前一定有元素大于基准值
        if(arr[i]<arr[pivot]){
            swap(arr,i,index);
            index++;
        }
   
    }
     // 将结束值与遍历到i互换,让i位于数列的中间 left--j小于基准 j+2---right 大于基准
     swap(arr,i,right);
     // 返回基准点的位子
     return index-1;
}

function swap(arr, i, j) {
    var temp = arr[i];
    arr[i] = arr[j];
    arr[j] = temp;
}

// 快速排序的第二种方法
function partition2(arr,low,high){
    let pivot = arr[low]; // 先选择第一个元素作为基准值
    // 使用两个指针双向地向中间遍历
    while(low<high){
        whilie(low<high && arr[high]>pivot){
            // 如果右侧指针大于基准值就正常移动
            --high;
        }
        // 不满足时说明找到了比基准值小元素 将其与左侧指针值互换,保证该值位于基准左侧
        arr[low] = arr[high];
        // 互换以后左侧指针开始移动 逻辑相同 将比基准值大的点移动到基准值右侧
        while (low < high && arr[low] <= pivot) {
            ++low;
          }
          arr[high] = arr[low];
    }
    // 记得还原基准值的位置
    arr[low] = pivot
    return arr
}
function quickSort2(arr,low,high){
    if(low<high){
        let pivot = partition2(arr,low,high);
        quickSort2(arr,low.pivot-1);
        quickSort2(arr,pivot+1,high);
    }
    return arr;
}

快速选择算法

const findKthLargest = (nums, k) => {
  return quickSelect(nums, 0, nums.length - 1, k);
};

const quickSelect = (nums, lo, hi, k) => {
  // 避免最坏情况发生
  const p = Math.floor(Math.random() * (hi - lo + 1)) + lo;
  swap(nums, p, hi);
  // 声明两个指针从左向右遍历
  let i = lo;
  let j = lo;
  // 将最右侧点视为pivot 遍历交互小于基准值的点到基准的左侧
  while (j < hi) {
    if (nums[j] <= nums[hi]) {
      swap(nums, i++, j);
    }
    j++;
  }
  // 注意复原基准值的位置
  swap(nums, i, j);
  // pivot 是我们要找的 Top k
  if (hi === k + i - 1) return nums[i];
  // Top k 在右边
  if (hi > k + i - 1) return quickSelect(nums, i + 1, hi, k);
  // Top k 在左边
  return quickSelect(nums, lo, i - 1, k - (hi - i + 1));
};

const swap = (nums, i, j) => ([nums[i], nums[j]] = [nums[j], nums[i]]);