排序算法

148 阅读5分钟

总结一下排序算法,还是需要多加练习,弄不清楚就画图体会。附上一张帅图

Cache_79c2839b04412a3d..jpg

排序

任何一种排序算法都没有优劣之分 排序不是比较大小,排序的本质是比较交换

冒泡排序

// 冒泡排序,一圈只能排出一个数

var arr = [4, 12, 3, 1, 2, 5, 8, 7, 10, 9];

function compare(a, b) {
  // 判断是否需要交换
  return a > b;
}

function exchange(arr, a, b) {
  // 将数组中的a,b位置的值交换
  var temp = arr[a];
  arr[a] = arr[b];
  arr[b] = temp;
}

function sort(arr) {
  for (var i = 0; i < arr.length - 1; i++) {
    //可能会问为什么i < arr.length - 1,因为你要保证arr[i + 1]有值
    if (compare(arr[i], arr[i + 1])) {
      exchange(arr, i, i + 1);
    }
  }
}
sort(arr);
console.log(arr);
/*
  [
    4, 3,  1, 2,  5,
    8, 7, 10, 9, 12
  ]
*/

完整的冒泡排序

// 冒泡排序
var arr = [4, 12, 3, 1, 2, 5, 8, 7, 10, 9];

function compare(a, b) {
  // 判断是否需要交换
  return a > b;
}

function exchange(arr, a, b) {
  // 将数组中的a,b位置的值交换
  var temp = arr[a];
  arr[a] = arr[b];
  arr[b] = temp;
}

function sort(arr) {
  for (var j = 0; j < arr.length; j++) {
    // 第几圈就少几次,因为后面的已定
    for (var i = 0; i < arr.length - 1 - j; i++) {
      if (compare(arr[i], arr[i + 1])) {
        exchange(arr, i, i + 1);
      }
    }
  }
}
sort(arr);
console.log(arr);
/*
 [Inversion
  1, 2, 3,  4,  5,
  7, 8, 9, 10, 12
 ]
*/

插入排序

一般也被称为直接插入排序Insert Sort。对于少量元素的排序,它是一个有效的算法。插入排序是一种最简单的排序方法,它的基本思想是将一个记录插入到已经排好序的有序表中,从而一个新的、记录数增 1 的有序表。在其实现过程使用双层循。

外层循环对除了第一个元素之外的所有元素遍历 内层循环对当前元素前面有序表进行待插入位置查找,并进行移动

let originArr = [2, 3, 10, 8, 5, 2];

function insertSort(arr) {
  for (var i = 1; i < arr.length; i++) {
    for (var j = i - 1; j >= 0 && arr[j] > arr[j + 1]; j--) {
      [arr[j], arr[j + 1]] = [arr[j + 1], arr[j]];
    }
  }
}

insertSort(originArr);
console.log(originArr);

二分查找(重要)

二分查找的条件不是必须为有序排列(刻板印象上二分一定有序,但是不是的,关键是需要找到能够二分的那个点)

主要就是要注意这个数组取值的范围,就比如我们取 end 赋值为 arr.lengtharr.length-1 的效果就有很多要注意的地方

let serachArr = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11];

function getNumIndex(arr, target) {
  let begin = 0;
  let end = arr.length; //注意这个end是取不到的,

  while (
    begin < end //这个end的边界情况是大于begin的,没有begin==end的情况
    // 如果end==arr.length-1那么这里的条件就是begin <= end因为相等时还有一个数,随之,下面更新边界情况也发生变化
  ) {
    let midIndex = Math.floor((end + begin) / 2); //(begin + end) >>> 1;
    if (arr[midIndex] == target) {
      return midIndex;
    } else if (arr[midIndex] > target) {
      end = midIndex;
    } else if (arr[midIndex] < target) {
      begin = midIndex + 1;
    }
  }
  return -1;
}

console.log(getNumIndex(serachArr, 8));

// 递归写法
function getNumIndex(arr, target) {
  function getIndex(arr, target, begin, end) {
    if (begin >= end) return -1;
    let midIndex = Math.floor((end + begin) / 2);
    if (arr[midIndex] == target) {
      return midIndex;
    } else if (arr[midIndex] > target) {
      return getIndex(arr, target, begin, midIndex);
    } else {
      return getIndex(arr, target, midIndex + 1, end);
    }
  }

  return getIndex(arr, target, 0, arr.length);
}

console.log(getNumIndex(serachArr, 8));

选择排序

选择排序的内层循环,每一圈选择一个最大的放到还没有排序的数的最后面

var arr = [4, 12, 3, 1, 2, 5, 8, 7, 10, 9];

function compare(a, b) {
  // 判断是否需要交换
  return a < b;
}

function exchange(arr, a, b) {
  // 将数组中的a,b位置的值交换
  var temp = arr[a];
  arr[a] = arr[b];
  arr[b] = temp;
}

// 选择排序
function selectSort(arr) {
  for (var i = 0; i < arr.length; i++) {
    var maxIndex = 0; //假设第一个数是最大值
    for (var j = 0; j < arr.length - i; j++) {
      if (compare(arr[maxIndex], arr[j])) {
        // 记录最大值
        maxIndex = j;
      }
    }
    // 一次循环过后交换
    exchange(arr, maxIndex, arr.length - 1 - i);
  }
}
selectSort(arr);
console.log(arr);

归并排序(分治思想)

把无序的数组细分为小数组,我们让小数组有序,然后把小数组在合并到一起。

// 归并排序
let arr = [2, 3, 4, 2, 34, 3, 2, 1, 78, 4];
// 先让子序列有序然后把子序列合并,就是整体的有序序列
function mergeSort(arr, l, r) {
  if (l == r) {
    return;
  }
  // 找到中点,然后左边分治、右边分治
  let mid = l + ((r - l) >> 1);
  mergeSort(arr, l, mid);
  mergeSort(arr, mid + 1, r);
  merge(arr, l, r, mid); // 在这个区间上有序,注意要有中间值做整合的

  function merge(arr, begin, end, mid) {
    let newArr = new Array(end - begin + 1); //开拓二外的子数组空间
    let index = 0; //子数组指针
    let i = begin; //左分治指针
    let j = mid + 1; //右分治指针
    // 以下是左右分治指针均未越界
    while (i <= mid && j <= end) {
      newArr[index++] = arr[i] <= arr[j] ? arr[i++] : arr[j++];
    }
    // 以下两种情况实际只会跑一个
    while (i <= mid) {
      // 左指针未越界
      newArr[index++] = arr[i++];
    }
    while (j <= end) {
      // 右指针未越界
      newArr[index++] = arr[j++];
    }
    // 这个时候我们的原数组还没有改变,一直在操作额外开辟的数组空间
    for (var n = 0; n < newArr.length; n++) {
      arr[begin + n] = newArr[n]; //把这个小数组的分治结果添加到原数组
    }
  }
}

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

求小和问题

let arr = [1, 3, 4, 2, 5];

// 求最小和
function toSum(arr, begin, end) {
  if (begin >= end) {
    return 0;
  }
  let mid = (end + begin) >> 1; //找中点,分治
  return (
    toSum(arr, begin, mid) +
    toSum(arr, mid + 1, end) +
    merge(arr, begin, end, mid)
  ); //合并

  function merge(arr, begin, end, mid) {
    let helpArr = new Array(end - begin + 1); //开辟额外的空间
    let l = begin; //左指针
    let r = mid + 1; //右指针
    let res = 0; //和
    let i = 0; //指针
    while (l <= mid && r <= end) {
      // 两个指针都没有越界
      res += arr[l] < arr[r] ? end - r + 1 * arr[l] : 0;
      helpArr[i++] = arr[l] < arr[r] ? arr[l++] : arr[r++];
    }
    while (l <= mid) {
      helpArr[i++] = arr[l++];
    }
    while (r <= end) {
      helpArr[i++] = arr[r++];
    }
    for (let i = 0; i < helpArr.length; i++) {
      arr[begin + i] = helpArr[i];
    }
    return res;
  }
}

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

leetcode 315 51(都是归并排序的变式)

简单快速排序

思想就是,创建leftright两个数组,分别以leader为基准,分来,然后再分开

var arr = [4, 12, 3, 1, 2, 5, 8, 7, 10, 9];

function quickSort(arr) {
  if (arr == null || arr.length == 0) return [];
  // 先选择一个基准
  var leader = arr[0];
  // 小的站左边,大的站右边
  var left = [];
  var right = [];
  for (var i = 1; i < arr.length; i++) {
    if (arr[i] < leader) {
      left.push(arr[i]);
    } else {
      right.push(arr[i]);
    }
  }
  // 递归
  left = quickSort(left);
  right = quickSort(right);
  //   拼接起来
  left.push(leader);
  return left.concat(right);
}

console.log(quickSort(arr));

标准快速排序

标准的快速排序不会创建数组,只会在原数组上进行操作.(比简单快速排序难理解)

  1. 定义一个leader
  2. 做两个指针leftright

我们希望左侧的都比leader小,右侧的都比leaderleft从左开始知道找到比leader大的数(min) right从右开始知道找到比leader小的数(max) 随后交换 min 和 max

第一遍交换

仍然是完成第一圈;
var arr = [4, 12, 3, 1, 2, 5, 8, 7, 10, 9];

function exchange(arr, a, b) {
  var temp = arr[a];
  arr[a] = arr[b];
  arr[b] = temp;
}

function quickSort(arr, begin, end) {
  if (begin >= end - 1) {
    return;
  }
  // 定义左右指针
  var left = begin;
  var right = end;
  do {
    // 这里面的leader默认就是arr[begin]
    do {
      left++; //左指针往右走
    } while (left < right && arr[left] < arr[begin]);
    do {
      right--; //右指针像左走
    } while (right > left && arr[right] > arr[begin]);
    if (left < right) {
      // 在完成上面两个do...while...之后,left指向第一个比leader大的数,right指向第一个比leader小的数
      exchange(arr, left, right);
    }
  } while (left < right);
  // leader要交换的点
  var centerIndex = left == right ? right - 1 : right;
  exchange(arr, begin, centerIndex);
}

// 标准快排
function balanceQuickSort(arr) {
  quickSort(arr, 0, arr.length); //传入需要排序的范围,在数组中arr.length取不到
}

balanceQuickSort(arr);
console.log(arr);

完成的标准快排

就是说再递归左边和右边

var arr = [4, 12, 3, 1, 2, 5, 8, 7, 10, 9];

function exchange(arr, a, b) {
  var temp = arr[a];
  arr[a] = arr[b];
  arr[b] = temp;
}

function quickSort(arr, begin, end) {
  if (begin >= end - 1) {
    return;
  }
  // 定义左右指针
  var left = begin;
  var right = end;
  do {
    // 这里面的leader默认就是arr[begin]
    do {
      left++; //左指针往右走
    } while (left < right && arr[left] < arr[begin]);
    do {
      right--; //右指针像左走
    } while (right > left && arr[right] > arr[begin]);
    if (left < right) {
      // 在完成上面两个do...while...之后,left指向第一个比leader大的数,right指向第一个比leader小的数
      exchange(arr, left, right);
    }
  } while (left < right);
  // leader要交换的点
  var centerIndex = left == right ? right - 1 : right;
  exchange(arr, begin, centerIndex);
  // 对左边再排(递归)
  quickSort(arr, begin, centerIndex);
  // 对右边再排(递归)
  quickSort(arr, centerIndex + 1, end);
}

// 标准快排
function balanceQuickSort(arr) {
  quickSort(arr, 0, arr.length); //传入需要排序的范围,在数组中arr.length取不到
}

balanceQuickSort(arr);
console.log(arr);

不创建额外空间对数组按照某值分为两类(双指针)

不创建额外空间,将数组分类

/**
 *
 * @param {*} arr 无序数组
 * @param {*} border 界定值
 */
// 不创建额外的空间,将数组按照边界值成两组
function pointerBorder(arr, border) {
  let p1 = 0; //左指针,控制边界
  let p2 = 0; //右指针,控制遍历
  while (p2 <= arr.length - 1) {
    if (arr[p2] <= border) {
      var temp = arr[p2];
      arr[p2++] = arr[p1];
      arr[p1++] = temp;
    } else {
      p2++;
    }
  }
}
pointerBorder(arr, 8);

荷兰国旗问题

就如荷兰国旗所示:我们需要将一个无序的数组,按照给定的某一个界定值target将数组分成三部分就好比荷兰的三色旗,分为小于等于大于三部分

let arr = [2, 3, 6, 8, 6, 7];
/**
 *
 * @param {*} arr 数组
 * @param {*} target 界定值
 */
function flagColorQues(arr, target) {
  let p1 = 0; //左区域指针
  let p2 = arr.length - 1; //右区域指针
  let i = 0; //遍历指针
  while (i <= p2 && p1 <= p2 && p1 <= i) {
    // 这个while循环条件也是要注意,当他们相等的情况下是有一个元素的,如果条件错误写出的结果也是错的
    if (arr[i] == target) {
      i++;
    } else if (arr[i] < target) {
      var temp = arr[i];
      arr[i++] = arr[p1];
      arr[p1++] = temp;
    } else if (arr[i] > target) {
      // 注意这个遍历指针不用++
      var temp = arr[i];
      arr[i] = arr[p2];
      arr[p2--] = temp;
    }
  }
}
flagColorQues(arr, 6);
console.log(arr);

堆排序

是一种特殊的二叉树结构也叫二叉堆,堆分为大根堆小根堆两种,由于的特性能够高效、快速的找到最大,最小值,常被用于优先队列中,也被用于著名的堆排序算法 大根堆的特性(小根堆相反)

  • 每个节点如果有孩子节点,那么它的的值大于或等于它的左、右孩子节点的值
  • 如果用数组表示大根堆那么数组的第一项就是这个数组的最大值,

排序

  • 首先是把原数组变成一个大根堆,那么这个数组的最大值我们就能够确定是数组的第一项
  • 随后将数组的第一项和的最后一位交换位置,堆的长度--,那么这个最大值就不再这个堆的范围内,后续也不会对他再做处理,因为我们要处理的就是还在堆中的元素
  • 在交换过后,在堆的范围内就不再是大根堆,这个时候就需要对新的arr[0]位置的元素放到合适的位置重新构成大根堆
  • 随后执行第二步第三步操作,直到堆里不再有数,那么我们每次操作都拿出了堆中的最大值并且放到了合适的地方

let arr = [12, 41, 42, 33, 45, 23, 7, 9, 10, 2]; //7
// 堆排序
function heapSort(arr) {
  if (arr.length < 2 || arr == null) return null;
  let heapSize = 0; //记录堆的深度,我们认为一开始没有堆
  for (var i = 0; i < arr.length; i++) {
    heapInsert(arr, i); //创建堆的过程
    heapSize++; //7
  }

  while (heapSize > 0) {
    // 堆里还有值,就需要让这个堆有序
    exchange(arr, 0, --heapSize); //交换最后一位
    heapLify(arr, 0, heapSize - 1); //创建的新的大根堆,不包括最后一位,影响下面left的循环条件包括右孩子
  }

  // 创建大根堆,向上
  function heapInsert(arr, index) {
    while (arr[index] > arr[Math.floor((index - 1) / 2)]) {
      exchange(arr, index, Math.floor((index - 1) / 2));
      index = Math.floor((index - 1) / 2);
    }
  }

  // 向下堆化
  function heapLify(arr, index, heapSize) {
    let left = index * 2 + 1;
    while (left <= heapSize) {
      //说明左孩子还在这个堆的范围内,
      // 这个index的下标的数需要更新,和他的两个孩子中大的那个交换
      // 有右孩子就需要比较,否则就是左孩子,这个条件我最开始就没有考虑完全就会出错
      let lagest =
        left + 1 <= heapSize && arr[left] < arr[left + 1] ? left + 1 : left;
      lagest = arr[index] > arr[lagest] ? index : lagest; //子节点比较完了和父节点比较,因为子节点的最大值不一定比他大
      if (lagest == index) {
        // 这种就不用交换
        break;
      }
      exchange(arr, index, lagest); //下标不一样的才交换
      index = lagest; //下标更新
      left = index * 2 + 1; //下一个循环
    }
  }

  // 交换
  function exchange(arr, a, b) {
    var temp = arr[a];
    arr[a] = arr[b];
    arr[b] = temp;
  }
}
heapSort(arr);
console.log(arr);