快速排序实现

快排1.0版本

  • 选数组最后一个元素为哨兵,将前面的元素(除了最后一个元素的数组)进行左右区间划分,使得前面元素比哨兵小的在左边,比哨兵大的在右边。划分区间算法

  • 快排每一轮保证了哨兵元素的位置正确

  • 最坏时间复杂度为O(n^2)

function quickSort(arr, left = 0, right = arr.length - 1) {
  if (left < right) {
    const pivotIndex = partition(arr, left, right);
    quickSort(arr, left, pivotIndex - 1);
    quickSort(arr, pivotIndex + 1, right);
  }
  return arr;
}

function partition(arr, left, right) {
  const pivot = arr[right];
  let i = left;
  // 分小于等于和大于区间
  for (let j = left; j < right; j++) {
    if (arr[j] <= pivot) {
      // Swap elements at indices i and j
      [arr[i], arr[j]] = [arr[j], arr[i]];
      i++;
    }
  }
  // 区分结束后,将小于区间的右边一个位置和最后一个哨兵元素位置互换
  // Swap pivot element with the element at index i
  [arr[i], arr[right]] = [arr[right], arr[i]];
  return i;
}

const arr = [4, 2, 6, 3, 7, 9, 0];
const sortedArray = quickSort(arr);
console.log(sortedArray); // 输出:[0, 2, 3, 4, 6, 7, 9]	

快排1.0版本另一种实现

  • 通过基准值查找,设定第一个为基准值,则比它小的放在左边数组,否则放在右边数组,下次迭代时,依旧按照此规则,则会一直找到最小的
  • 因为每次根据基准值分左右,所以最终基准值就会是最值,没有比最值大或小的,所以递归调用的Quick会返回空[],因为先执行小的递归,再执行大的递归,递归进入回滚,将每次分组的最小基准值依次连接起来,最后形成从小到大的排序实现

时间复杂度

  • 在最佳情况下,每一次都平分整个数组。假设数组有 n 个元素,其递归的深度就为 log2n + 1,时间复杂度为:o(nlog2n)。2为底数
  • 最坏情况下,假设数组已经排好序,且为所要排序的逆序,每次选择基准元素时总是选择第一个元素或者最后一个元素,那么每次都会有一个子集是空的,递归的层数将达到 n,最后导致算法的时间复杂度退化为 O(n²)。
(() => {

  function Quick(list) {
    
    if (list.length == 0) {
      return [];
    }

    let pivot = list[0];
    let lesser = [];
    let greater = [];

    for (let i = 1; i < list.length; i++){ //快排:安装基准值排序,小于基准值放在左边,大于放在右边
      if (list[i] < pivot) {
        lesser.push(list[i]);
      } else {
        greater.push(list[i]);
      }
    }

    return Quick(lesser).concat(pivot,Quick(greater));


  }

  let res=Quick([44,75,23,43,55,12,64,77])

  console.log(res);


})()

快排2.0版本

  • 利用最上面链接的荷兰国旗问题,每次取数组的最后一个元素为哨兵元素,将哨兵元素前面的区间按照小于哨兵元素、等于哨兵元素、大于哨兵元素划分,然后将哨兵元素和大于区间的第一个元素做交换,这样每次处理了一批等于哨兵元素的位置,然后将小于区间、大于区间按照开始逻辑进行递归
    • 也可以不交换,每次直接取最后一个元素为基准,排序整个数组,然后将小于区间、大于区间按照开始逻辑进行递归
  • 最坏时间复杂度为O(n^2)
let arr = [3, 5, 6, 7, 8, 3, 4, 5];

//划分小于、等于、大于区间
function cutting(arr, num, pleft, pright) {
  let left = pleft - 1;
  let right = pright + 1;

  for (let i = pleft; i < pright + 1; i++) {
    if (i == right) {
      break;
    }
    if (arr[i] < num) {
      [arr[left + 1], arr[i]] = [arr[i], arr[left + 1]];
      left++;
    } else if (arr[i] == num) {
      continue;
    } else {
      [arr[i], arr[right - 1]] = [arr[right - 1], arr[i]];
      right--;
      //使得下轮比较从右边交换过来的数
      i--;
    }
  }

  //返回小于、大于区间的右、左边界
  return [left, right];
}

function sort(arr, left, right) {
  if (left >= right) {
    return;
  }

  let mid = cutting(arr, arr[right], left, right);

  sort(arr, left, mid[0]);
  sort(arr, mid[1], right);
}

sort(arr, 0, arr.length - 1);
console.log(arr);

快排3.0版本

  • 因为如果数组是逆序,每次取最后一个进行排序,会产生最坏情况O(N^2)
  • 从数组中任意取一个数,和最后一个元素进行交换后,在进行上面的排序
  • 因为是任意取一个数,所以划分的区间可能是1/3和2/3,也可能是1/2和1/2,把每一种情况求概率累加再求长期期望,得到的结果是O(N*logN)和每次根据数组中点递归,通过Master公式求得的时间复杂度一致
let arr = [3, 5, 6, 7, 8, 3, 4, 5];

//划分小于、等于、大于区间
function cutting(arr, num, pleft, pright) {
  let left = pleft - 1;
  let right = pright + 1;

  for (let i = pleft; i < pright + 1; i++) {
    if (i == right) {
      break;
    }
    if (arr[i] < num) {
      [arr[left + 1], arr[i]] = [arr[i], arr[left + 1]];
      left++;
    } else if (arr[i] == num) {
      continue;
    } else {
      [arr[i], arr[right - 1]] = [arr[right - 1], arr[i]];
      right--;
      //使得下轮比较从右边交换过来的数
      i--;
    }
  }

  //返回小于、大于区间的右、左边界
  return [left, right];
}

function sort(arr, left, right) {
  if (left >= right) {
    return;
  }
  //取区间随机值和最后一个数交换
  let rand = left + parseInt(Math.random() * (right - left + 1));
  [arr[right], arr[rand]] = [arr[rand], arr[right]];
  
  let mid = cutting(arr, arr[right], left, right);

  sort(arr, left, mid[0]);
  sort(arr, mid[1], right);
}

sort(arr, 0, arr.length - 1);
console.log(arr);

你可能感兴趣的:(前端算法,数据结构)