快速排序的js实现

169 阅读5分钟

一、快速排序思想

(1)在数据集之中,选择一个元素作为"基准"(pivot)。

(2)所有小于"基准"的元素,都移到"基准"的左边;所有大于"基准"的元素,都移到"基准"的右边。

(3)对"基准"左边和右边的两个子集,不断重复第一步和第二步,直到所有子集只剩下一个元素为止。

快速排序是先将一个元素排好序,然后再将剩下的元素排好序,类似二叉树的前序遍历。

时间复杂度:递归层数是log(n), 所以平均时间复杂度是O(nlog(n))。对于排序好的数组,递归层数是n,时间复杂度会降为O(n^2)

void sort(int[] nums, int lo, int hi) {
    if (lo >= hi) {
        return;
    }
    // 对 nums[lo..hi] 进行切分
    // 使得 nums[lo..p-1] <= nums[p] < nums[p+1..hi]
    int p = partition(nums, lo, hi);
    // 去左右子数组进行切分
    sort(nums, lo, p - 1);
    sort(nums, p + 1, hi);
}
/* 二叉树遍历框架 */
void traverse(TreeNode root) {
    if (root == null) {
        return;
    }
    /****** 前序位置 ******/
    print(root.val);
    /*********************/
    traverse(root.left);
    traverse(root.right);
**}**
  • 相关1:冒泡排序

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

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

  • 相关2:归并排序

    归并排序就是先把左半边数组排好序,再把右半边数组排好序,然后把两半数组合并,类似二叉树的后序遍历

// 定义:排序 nums[lo..hi]
void sort(int[] nums, int lo, int hi) {
    if (lo == hi) {
        return;
    }
    int mid = (lo + hi) / 2;
    // 利用定义,排序 nums[lo..mid]
    sort(nums, lo, mid);
    // 利用定义,排序 nums[mid+1..hi]
    sort(nums, mid + 1, hi);

    /****** 后序位置 ******/
    // 此时两部分子数组已经被排好序
    // 合并两个有序数组,使 nums[lo..hi] 有序
    merge(nums, lo, mid, hi);
    /*********************/
}

// 将有序数组 nums[lo..mid] 和有序数组 nums[mid+1..hi]
// 合并为有序数组 nums[lo..hi]
void merge(int[] nums, int lo, int mid, int hi);


/* 二叉树遍历框架 */
void traverse(TreeNode root) {
    if (root == null) {
        return;
    }
    traverse(root.left);
    traverse(root.right);
    /****** 后序位置 ******/
    print(root.val);
    /*********************/
}

二、例举简单版-快速排序(阮一峰博客)

var quickSort = function(arr) {

  if (arr.length <= 1) { return arr; }

  var pivotIndex = Math.floor(arr.length / 2);

  var pivot = arr.splice(pivotIndex, 1)[0];

  var left = [];

  var right = [];

  for (var i = 0; i < arr.length; i++){

    if (arr[i] < pivot) {

      left.push(arr[i]);

    } else {

      right.push(arr[i]);

    }

  }

  return quickSort(left).concat([pivot], quickSort(right));

};

每次递归都会新建left、right数组,大小为n级别。 所以此算法空间复杂度是O(nlog(n))。

三、改良版-快速排序(基于阮老师博客)

var quickSort = function (arr, compare) {
  if (arr.length <= 1) { return arr; } // base条件
  var pivotIndex = Math.floor(arr.length / 2); // 1. 选择pivot基准
  var pivot = arr[pivotIndex];
  var left = [];
  var right = [];
  // 所有小于"基准"的元素,都移到"基准"的左边;所有大于"基准"的元素,都移到"基准"的右边。
  for (var i = 0; i < arr.length; i++) {
    if (i === pivotIndex) continue
    if (compare(arr[i], pivot) < 0) {
      left.push(arr[i]);
    } else if (compare(arr[i], pivot) > 0) {
      right.push(arr[i]);
    } else if (compare(arr[i], pivot) === 0) {
      // 保证排序的稳定性
      if (i < pivotIndex) {
        left.push(arr[i]);
      } else {
        right.push(arr[i]);
      }
    }
  }

  // 3. 对"基准"左边和右边的两个子集,不断重复第一步和第二步,直到所有子集只剩下一个元素为止
  return quickSort(left, compare).concat([pivot], quickSort(right, compare));
};

// 测试
const testArr = [
  {
    id: 1,
    number: 5
  },
  {
    id: 2,
    number: 3
  },
  {
    id: 3,
    number: 3
  },
  {
    id: 4,
    number: 3
  },
  {
    id: 5,
    number: 1
  },
]
function compare (a, b) {
  return a.number - b.number
}

console.log(quickSort(testArr, compare))

可以看到改良后的快速排序有这几处优点:

  • 1. 排序是稳定的。(利用额外的空间,获得了排序稳定性)
    1. 优化了以下的一处逻辑。
/** 原本 **/
var pivot = arr.splice(pivotIndex, 1)[0]; // 时间复杂度是O(n)

/* 改为  */
var pivot = arr[pivotIndex];
// 并在for循环额外加上
if (i === pivotIndex) continue

但是空间复杂度依然过高O(nlog(n))!

四、原地快速排序--挖坑法(但是无法解决稳定性问题)

image.png

function quickSort(arr, startIndex, endIndex) {
  if (startIndex >= endIndex) return
  const pivotIndex = partition(arr, startIndex, endIndex)

  // 分治法递归数组的两部分
  quickSort(arr, startIndex, pivotIndex - 1)
  quickSort(arr, pivotIndex + 1, endIndex)
}
function partition(arr, startIndex, endIndex) {
  // 取第一个位置元素作为基准
  const pivot = arr[startIndex]

  // 坑的位置,初始等于pivot的位置
  let index = startIndex
  let left = startIndex
  let right = endIndex

  // left 指针和 right 指针没有交错
  while (right >= left) {
    // right 指针从右向左移动
    while (right >= left) {
      if (arr[right] < pivot) {
        arr[index] = arr[right]
        index = right
        left++
        break
      }
      right--
    }
    // left 指针从左向右移动
    while (right >= left) {
      if (arr[left] > pivot) {
        arr[index] = arr[left]
        index = left
        right--
        break
      }
      left++
    }
  }
  arr[index] = pivot
  return index
}

const arr = [3, 2, 1, 5, 6, 4]
quickSort(arr, 0, arr.length - 1)
console.log('dig', arr)

参考:zhuanlan.zhihu.com/p/63202860

五、原地快速排序--指针交换法(但是无法解决稳定性问题)

function quickSort_exchange(arr, startIndex, endIndex) {
  if (startIndex >= endIndex) return
  const pivotIndex = partition(arr, startIndex, endIndex)

  // 分治法递归数组的两部分
  quickSort_exchange(arr, startIndex, pivotIndex - 1)
  quickSort_exchange(arr, pivotIndex + 1, endIndex)
}

// 指针交换法
function partition(arr, startIndex, endIndex) {
  // 取第一个位置元素作为基准
  const pivot = arr[startIndex]
  let left = startIndex
  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) {
      const p = arr[left]
      arr[left] = arr[right]
      arr[right] = p
    }
  }
  // pivot和指针重合点交换
  arr[startIndex] = arr[left]
  arr[left] = pivot

  return left
}

const arr2 = [3, 2, 1, 5, 6, 4]
quickSort_exchange(arr2, 0, arr2.length - 1)
console.log('exchange', arr2)

参考:www.cxyxiaowu.com/5262.html