算法学习:用JavaScript写排序

该内容是学习B站左神的算法课生产的。 

时间复杂度 O (读big O)

常数操作的时间 —— 一个操作如果和样本的数据量没有关系,每次都是固定时间完成的操作,叫做常数操作。

时间复杂度为一个算法流程中,常熟操作数量的一个指标。用O(读big O)来表示,首先要对算法流程非常熟悉,写出算法中发生了多少常数操作,然后总结成常数操作数量的表达式,表达式中,不要低阶项、常数,也不要高阶项的系数,只要高阶项,剩下部分如果是 f(n),那么时间复杂度为O(f(n))

评价一个算法流程的好坏,先看时间复杂度的指标,再分析不同数据样本的实际运行时间,也就是“常数项时间”。


1. 选择排序

遍历未排序的数据寻找最小/大的数,放入已排序的末尾

时间复杂度:O(n^2) 与数据无关

空间复杂度:O(1)

function select_sort(arr) {
  if (arr.length < 2 || arr == null) return;
  for (let i = 0; i < arr.length; i++) {
    let minIndex = i; // 假设当前i为最小index
    for (let j = i + 1; j < arr.length; j++) {
      if (arr[minIndex] > arr[j]) { //查询后边的值发现比假设值更小
        swap(arr, minIndex, j);// 交换
      }
    }
  }
  return arr;
}
function swap(arr, a, b) {
  arr[a] = arr[a] ^ arr[b];
  arr[b] = arr[a] ^ arr[b];
  arr[a] = arr[a] ^ arr[b];
}

2. 冒泡排序

遍历数据,两两对比,将小/大的放前面,比较n轮

时间复杂度:O(n^2) 与数据无关

空间复杂度:O(1)

function pop_sort(arr) {
  if (arr == null || arr.length < 2) return;
  const len = arr.length;
  // for (let i = 0; i < len; i++) {
  for (let i = len; i > 0; i--) {
    for (let j = 0; j < i; j++) {
      if (arr[j] > arr[j + 1]) {
        swap2(arr, j, j + 1)
      }
    }
  }
  return arr;
}
function swap2(arr, i, j) {
  let temp = arr[i];
  arr[i] = arr[j];
  arr[j] = temp;
}

3. 插入排序

从数据左边开始,每个数据都与前面的数做比较,谁小/大就交换位置,直至前面无数据对比或不大/小于

时间复杂度:O(n^2) 受数据影响([1,2,3,4][4,3,2,1]操作次数不同),但是以数据最差常数操作来估计

空间复杂度:O(1)

function insert_sort(arr) {
  if (arr == null || arr.length < 2) return;
  const len = arr.length;
  for (let i = 1; i < len; i++) {
    while (arr[i] < arr[i - 1] && i >= 0) {
      swap(arr, i, i - 1);
      i--
    }
  }
  return arr;
}
function swap(arr, i, j) {
  arr[i] = arr[i] ^ arr[j];
  arr[j] = arr[i] ^ arr[j];
  arr[i] = arr[i] ^ arr[j];
}

4. 归并排序

利用递归,左边排好序,右边排好序,再让整体有序

时间复杂度:O(N*logN)

空间复杂度:O(N)

const arr1 = [1, 4, 2, 4, 6, 7, 9];
​
// 左边排序, 右边排序, 左右对比, 创建一个数组, 谁小,谁先放进去 
function merge_sort(arr, l, r) {
  if (arr.length < 2 || arr == null) return arr;
  if (l == r) return;
  const mid = l + ((r - l) >> 1);
​
  merge_sort(arr, l, mid);
  merge_sort(arr, mid + 1, r);
  return merge(arr, l, mid, r);
}
function merge(arr, l, m, r) {
  let sortArr = [];
  let i = 0
  let p1 = l;
  let p2 = m + 1;
  while (p1 <= m && p2 <= r) {
    sortArr[i++] = arr[p1] <= arr[p2] ? arr[p1++] : arr[p2++];
  }
  while (p1 <= m) {
    sortArr[i++] = arr[p1++]
  }
  while (p2 <= r) {
    sortArr[i++] = arr[p2++]
  }
  for (let i = 0; i < sortArr.length; i++) {
    arr[l + i] = sortArr[i]
  }
  return arr;
}
console.log(merge_sort(arr1, 0, arr1.length - 1))

扩展:

  • 小和问题

    在一个数组中,每一个数左边比当前数小的数累加起来,叫做这个数组的小和。求数组小和。

const arr2 = [1, 4, 2, 3, 8, 5];
​
function getSum(arr, L, R) {
  if (arr.length < 2 || arr == null) return;
  if (L == R) return 0;
​
  const M = L + ((R - L) >> 1);
  return getSum(arr, L, M) + getSum(arr, M + 1, R) + add_up(arr, L, M, R);
}
function add_up(arr, L, M, R) {
  let help = [];// 需要先排序
  let p1 = L;
  let p2 = M + 1;
  let res = 0;
  
  while (p1 <= M && p2 <= R) {
    if (arr[p1] < arr[p2]) {
      res += arr[p1] * (R - p2 + 1);
      help.push(arr[p1]);
      p1++
    } else {
      help.push(arr[p2]);
      p2++
    }
    // res += arr[p1] < arr[p2] ? arr[p1] * (R - p2 + 1) : 0;
    // arr[p1] < arr[p2] ? help.push(arr[p1++]) : help.push(arr[p2++])
  }
  while (p1 <= M) {
    help.push(arr[p1++])
  }
  while (p2 <= R) {
    help.push(arr[p2++])
  }
  console.log(res, '---');
  console.log(help,'===', arr);
  // 将原数组左右两边排好序
  for (let i=0;i<help.length;i++) {
    arr[i + L] = help[i];
  }
  return res;
}
​
console.log(getSum(arr2, 0, arr2.length - 1));

  • 逆序对问题

    在一个数组中,左边的数如果比右边的数大,则这两个数构成一个逆序对,打印所有的逆序对。

const arr2 = [1, 4, 2, 3, 8, 5];
​
function getSum(arr, L, R) {
  if (arr.length < 2 || arr == null) return;
  if (L == R) return 0;
​
  const M = L + ((R - L) >> 1);
  return getSum(arr, L, M) + getSum(arr, M + 1, R) + add_up(arr, L, M, R)
}
function add_up(arr, L, M, R) {
  let help = [];// 需要先排序
  let p1 = L;
  let p2 = M + 1;
  let res = 0;
  
  while (p1 <= M && p2 <= R) {
    res += arr[p1] > arr[p2] ? 1 : 0;
    arr[p1] < arr[p2] ? help.push(arr[p1++]) : help.push(arr[p2++]);
  }
  while (p1 <= M) {
    help.push(arr[p1++])
  }
  while (p2 <= R) {
    help.push(arr[p2++])
  }
  // 将原数组左右两边排好序
  for (let i=0;i<help.length;i++) {
    arr[i + L] = help[i];
  }
  return res;
}
​
console.log(getSum(arr2, 0, arr2.length - 1));

  • 荷兰国旗问题

    • 给定一个数组arr,一个数num,请把小于等于num的数放在数组的左边,大于num的数放在数组的右边。要求空间复杂度为O(1),时间复杂度为O(N)

    • 给定一个数组arr,一个数num,请把小于num的数放在数组的左边,等于num的数放在数组的中间,大于num的数放在数组的右边。要求空间复杂度为O(1),时间复杂度为O(N)

const arr = [1, 2,3,4,2,2,1,5];
const target = 3;
​
function classification(arr, target) {
  let i = 0;
  let small = 0;
  let big = arr.length - 1;
  while (i <= big) {
    if (arr[i] < target) {
      swap(arr, small++, i++);
    } else if (arr[i] > target) {
      swap(arr, big--, i)
    } else {
      i++
    }
  }
  console.log(arr);
}
function swap(arr, i, j) {
  let temp = arr[i];
  arr[i] = arr[j];
  arr[j] = temp;
}
classification(arr, target)

5. 快速排序

  • 1.0版本,在一个数组中,总是拿某一个范围内的最后一个数做划分,分为大于这个数的和小于这个数的,这个流程做递归。

  • 2.0版本,在1.0版本中,分为大于这个数、等于这个数、小于这个数。

  • 1.0和2.0版本的时间复杂度在最差情况下为O(N^2)。

  • 3.0版本,在数组中随机选一个数和最后一个数做交换,以它做划分。O(NlogN)

  • 额外空间复杂度O(logN)

let arr = [2, 5, 1, 8, 3, 4, 7, 1, 3];
quick_sort(arr, 0, arr.length - 1);
console.log(arr);// [1, 1, 2, 3, 3, 4, 5, 7, 8]
​
function quick_sort(arr, l, r) {
  if (l < r) {
    swap(arr, l + Math.floor(Math.random() * (r-l+1)), r); // 随机选一个数和最后一个做交换
    let p = partition(arr, l, r); //  划分区域的左边界和右边界
    quick_sort(arr, l, p[0] - 1); //  < 区
    quick_sort(arr, p[1] + 1, r); //  > 区
  }
}
​
function partition(arr, l, r) {
  let less = l - 1; // < 区右边界
  let more = r; // >区左边界
  while(l < more) { // l表示当前数的位置; arr[r]表示划分值
    if (arr[l] < arr[r]) {
      swap(arr, l++, ++less);
    } else if (arr[l] > arr[r]) {
      swap(arr, l, --more);
    } else {
      l++
    }
  }
  swap(arr, more, r);
  return [less + 1, more]
}
​
function swap(arr, i, j) {
  let temp = arr[i];
  arr[i] = arr[j];
  arr[j] = temp;
}

扩:归并排序和快速排序对比

归并排序快速排序
实现将数组分成两个子数组分别排序,并将有序的子数组归并来将整个数组排序。当两个子数组都有序时整个数组也就有序了。
递归时机递归调用发生在处理整个数组之前。递归调用发生在处理整个数组之后。
数组处理数组等分为两半切分(partition)位置取决于数组内容

6. 堆排序

  1. 先让整个数组都变成大根堆结构

    • 从上到下heap_insert(),时间复杂度为O(N*logN)

    • 从下到上heapify(),时间复杂度为O(N)

  2. 把堆的最大值和堆末尾的值进行交换,然后减少堆的大小,继而去调整重新变成大根最,一直周而复始,时间复杂度为O(N*logN)

  3. 堆的大小减少为0时,就拍好序了。

  • 时间复杂度O(NlogN)

  • 空间复杂度O(1)

let arr = [2, 1, 5, 3, 2];
console.log(heap_sort(arr))
// 堆排序
function heap_sort(arr) {
  if (arr == null || arr.length < 2) return;
  
  // heapInsert 把数组变成大根堆
  // 方法一: 利用heap_insert, 从上上找
  //for (let i = 0; i < arr.length; i++) {
    //heap_insert(arr, i); // O(logN)
  //}
  // 方法二: 利用heapify, 从下到上, 不断向下将最大值找到, 实际使用效率更高
  for(let i = arr.length; i >= 0; i--) {
    heapify(arr, i, arr.length)
  }
  
  let heapSize = arr.length;
  // 把第一个数和最后一个调换位置, heapSize - 1, 调用heapify()
  swap(arr, 0, --heapSize)
  while (heapSize > 0) {
    heapify(arr, 0, heapSize)
    swap(arr, 0, --heapSize)
  }

  return arr;
}
function heapify(arr, index, heapSize) {
  // 左孩子
  let left = index * 2 + 1;
  while (left < heapSize) { // 判断是否有孩子,左孩子是否越界
    // 两个孩子谁大, 谁把下标给maxVal  得先判断右孩子是否存在  得注意,当条件不成立时, 下标应该为左孩子
    let maxVal = left + 1 < heapSize && arr[left + 1] > arr[left] ? left + 1 : left;
    // 父亲和较大孩子的值谁大, 谁把下标给maxVal
    maxVal = arr[index] > arr[maxVal] ? index : maxVal;
    // 父值等于最大值, 直接break
    if (maxVal == index) break;
    swap(arr, index, maxVal);
    index = maxVal;
    left = index * 2 + 1;
  }
}
function heap_insert(arr, index) {
  let fa = Math.trunc((index - 1) / 2);
  while (arr[index] > arr[fa]) {
    swap(arr, index, fa);
    index = fa;
    fa = Math.trunc((index - 1) / 2);
  }
  // while (arr[index] > arr[Math.trunc((index - 1) / 2)]) {
  //   swap(arr, index, Math.trunc((index - 1) / 2))
  //   index = Math.trunc((index - 1) / 2)
  // }
}
function swap(arr, i, j) {
  let temp = arr[i];
  arr[i] = arr[j];
  arr[j] = temp;
}

未完......

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值