排序js版

605 阅读5分钟

1、冒泡排序的优化

列表每相邻的数,若前面的大于后面的,则交换两个数。

长度为【length】的列表,最后剩下的一个数是默认有序的,第一次for循环 i 的边界为length - 1

经过第 i 次循环,有序部分的长度为 i,无序部分长度为length - i

第二层for循环j的边界为 length - i - 1

// 冒泡排序的优化
function demo(arr) {
  for (let i = 0; i < arr.length - 1; i++) {
    let flag = false;
    for (let j = 0; j < arr.length - i - 1; j++) {
      if (arr[j] > arr[j + 1]) {
        [arr[j], arr[j + 1]] = [arr[j + 1], arr[j]];
        flag = true; //如果元素换位,则flag变为true
        // console.log('观察代码运行的过程', arr);
      }
    }
    if (!flag) {
      console.log(`flag为false,余下列表没有换位,余下列表为有序列表`);
      break; //跳出i的循环
    }
  }
  return arr;
}
// const arr = [4, 5, 3, 1, 2];
const arr = [1, 2, 3, 4, 5];
console.log(demo(arr));

2、选择排序

记录无序区最小值的索引,默认无序区索引最小值为无序区第一个。

将得到的无序区最小值和无序区的第一个值交换,意为将无序区的第一个位置变为有序区,-->左侧有序区间扩增。

function demo(arr) {
  for (let i = 0; i < arr.length - 1; i++) {
    let minIndex = i; //无序区的第一个索引先记作为本次循环的最小值
    for (let j = i + 1; j < arr.length; j++) {
      if (arr[minIndex] > arr[j]) {
        minIndex = j; //发现更小的j
      }
    }
    // 最小值就是无序区的第一个数字,minIndex没有发生改变,是不需要换位的(优化)
    if (i !== minIndex) {
      [arr[i], arr[minIndex]] = [arr[minIndex], arr[i]];
    }
    // console.log('观察代码运行的过程', arr);
  }
  return arr;
}
const arr = [4, 5, 3, 1, 2];
// const arr = [1, 2, 3, 4, 5];
console.log(demo(arr));

3、插入排序

想象自己在打扑克,有手就行。

function demo(arr) {
  //摸牌的范围是1~length
  for (let i = 1; i < arr.length; i++) {
    tmp = arr[i]; //记录摸到的牌
    j = i; //手中牌的下标
    // 左侧的牌比摸到的牌大,则将左侧的排依次向右移
    while (j > 0 && arr[j - 1] > tmp) {
      arr[j] = arr[j - 1];
      j--;
    }
    // 左侧的牌小于等于摸到的牌,将摸到的牌插入左侧的牌之后
    arr[j] = tmp;
    // console.log('观察代码运行的过程', arr);
  }
  return arr;
}

const arr = [4, 5, 3, 1, 2];
// const arr = [1, 2, 3, 4, 5];
console.log(demo(arr));

4、快速排序

function demo(arr, left = 0, right = arr.length - 1) {
  if (left < right) {
    let mid = partition(arr, left, right);
    demo(arr, left, mid - 1);//递归mid的左侧部分
    demo(arr, mid + 1, right);//递归mid的右侧部分
  }
  return arr;
  // 归位函数
  function partition(arr, left, right) {
    tmp = arr[left]; //选定一个数字作为基数
    while (left < right) {
      while (arr[right] >= tmp && left < right) {
        right--;//1.right大于基数,则right指针左移(2.right忽略比基数大的,只去找比基数小的,找到后将其与left指针交换)
      }
      arr[left] = arr[right];//2.right指针找到小于基数的数字,将此数字与left交换
      while (arr[left] <= tmp && left < right) {
        left++;
      }
      arr[right] = arr[left];
    }
    arr[left] = tmp;
    return left;
  }
}
const arr = Array.from(Array(5000), (v, k) => k).sort((a, b) => Math.random() - Math.random());
// console.log('未排序的', arr);
console.log(demo(arr));

5、堆排序

堆是具有以下性质的完全二叉树:每个结点的值都大于或等于其左右孩子结点的值,称为大顶堆。

思路:将大顶堆的根节点与最后一个叶子节点交换的,砍掉最后一个节点,此时根节点下的两个子树都是大顶堆,但是加上根节点不符合大顶堆的性质,为了维护大顶堆的性质,从根节点开始heapify。

// 交换值
const swap = (A, i, j) => {
  let temp = A[i];
  A[i] = A[j];
  A[j] = temp;
};
// 维护最大堆性质
const maxHeapify = (A, heapSize, i) => {
  let l = i * 2 + 1;
  let r = i * 2 + 2;
  let largest = i; //根、左子、右子比较,择最大与根交换
  if (l < heapSize && A[l] > A[i]) {
    largest = l;
  }
  if (r < heapSize && A[r] > A[largest]) {
    largest = r;
  }
  if (largest !== i) {
    swap(A, i, largest);
    maxHeapify(A, heapSize, largest);//largest标记了比根大的子节点,继续向下。。。maxHeapify
  }
  return A;
};
//从最小子树开始向上build大顶堆
const buildMaxHeap = A => {
  //边界:由完全二叉树性质得知,子节点i找父节点,父节点为(i-1)/2,数组索引0~n-1.故(n-2)/2==>Math.floor(n/2)-1为最后一个叶子节点的根。
  for (let i = Math.floor(A.length / 2) - 1; i >= 0; i--) {
    maxHeapify(A, A.length, i);
  }
};
const heapSort = A => {
  buildMaxHeap(A);
  A.heapSize = A.length;
  for (let i = A.length - 1; i >= 0; i--) {
    swap(A, 0, i); //交换最大的和最后一个叶子节点
    A.heapSize--;
    maxHeapify(A, A.heapSize, 0); //根节点不再是大根堆,从根开始heapify
  }
  return A;
};
console.log(heapSort([3, 4, 5, 1, 5, 7, 2]));

5.1打印前k大个数字(需要用到小顶堆)

// 前k大问题,要用到小根堆
// 交换值
const swap = (A, i, j) => {
  let temp = A[i];
  A[i] = A[j];
  A[j] = temp;
};
// 维护最小堆性质
const minHeapify = (A, heapSize, i) => {
  let l = i * 2 + 1;
  let r = i * 2 + 2;
  let largest = i; //根、左子、右子比较,择最小与根交换
  if (l < heapSize && A[l] < A[i]) {
    largest = l;
  }
  if (r < heapSize && A[r] < A[largest]) {
    largest = r;
  }
  if (largest !== i) {
    swap(A, i, largest);
    minHeapify(A, heapSize, largest); //largest标记了比根小的子节点,继续向下。。。minHeapify
  }
  return A;
};
const topk = (A, k) => {
  // 前k个元素构建成最小堆
  let minHeap = A.splice(0, k);
  for (let i = Math.floor(minHeap.length / 2) - 1; i >= 0; i--) {//从第一个非叶节点向上执行
    minHeapify(minHeap, minHeap.length, i);
  }
  minHeap.heapSize = minHeap.length;
  // 剩余部分依次和小顶堆的顶部比较,大于小顶部堆顶部的会代替小顶堆顶部,然后heapify
  for (let i = 0; i < A.length; i++) {
    if (A[i] > minHeap[0]) {
      minHeap[0] = A[i];
      minHeapify(minHeap, minHeap.heapSize, 0);
    }
  }
  return minHeap; //因为小顶堆的缘故,这里打出来时倒序的
};
console.log(topk([3, 4, 5, 1, 8, 7, 2], 5));

6、归并排序(先拆分再合并,顾名思义)

// 一次归并
const merge = (li, low, mid, high) => {
  let i = low;
  let j = mid + 1;
  arr = []; //临时变量,非原地排序
  while (i <= mid && j <= high) {
    if (li[i] < li[j]) {
      arr.push(li[i]);
      i++;
    } else {
      arr.push(li[j]);
      j++;
    }
  }
  while (i <= mid) {
    arr.push(li[i]);
    i++;
  }
  while (j <= high) {
    arr.push(li[j]);
    j++;
  }
  // li.splice(low, arr.length, ...arr);
  li.splice(low, high - low + 1, ...arr); //将数组中某一部分替换成排序好的顺序
};

const mergeSort = (li, low, high) => {
  //至少有两个元素,
  if (low < high) {
    let mid = Math.floor((low + high) / 2);
    mergeSort(li, low, mid); //递归左侧
    mergeSort(li, mid + 1, high); //递归右侧
    merge(li, low, mid, high); //将左侧和右侧合并起来
  }
  return li;
};
const li = Array.from(Array(5000), (v, k) => k).sort((a, b) => Math.random() - Math.random());
console.time();
console.log(mergeSort(li, 0, li.length - 1));
console.timeEnd();

7、希尔排序(插入的基础上做切片操作,时间复杂度基于切片的大小)

function insertSort(li, gap) {
  for (let i = gap; i < li.length; i++) {
    let tmp = li[i];
    let j = i - gap;
    while (j >= 0 && tmp < li[j]) {
      li[j + gap] = li[j];
      j -= gap;
    }
    li[j + gap] = tmp;
  }
}
function shellSort(li) {
  let gap = Math.floor(li.length / 2);
  while (gap > 0) {
    insertSort(li, gap);
    gap = Math.floor(gap / 2);
  }
  return li;
}
const li = Array.from(Array(5000), (v, k) => k).sort((a, b) => Math.random() - Math.random());
console.time();
console.log(shellSort(li));
console.timeEnd();

8、计数排序(利用数组索引,记录出现次数)

function demo(arr, maxCount) {
  let count = new Array(maxCount + 1); //桶的容量
  let arrIndex = 0;
  for (let i = 0; i < arr.length; i++) {
    if (!count[arr[i]]) {
      count[arr[i]] = 1;
    } else {
      count[arr[i]] += 1;
    }
  }
  for (let j = 0; j < count.length; j++) {
    while (count[j] > 0) {
      arr[arrIndex++] = j;
      count[j]--;
    }
  }
  return arr;
}
const li = Array.from(Array(101), (v, k) => k).sort((a, b) => Math.random() - Math.random());
// const li = [1, 3, 2, 4, 1, 2, 3, 1, 3, 5];
console.time();
console.log(demo(li, 100));
console.timeEnd();

9、桶排序1.0

将元素分在不同的桶中,在对每个桶中的元素排序

因为桶本身是有序的(例如:0 ~ 99为0号桶,100 ~ 199为1号桶),桶内的元素也是有序的,将所有桶的集合扁平化即为结果

function bucketSort(li, n = 100, maxNum = 10000) {
  // 创建二维数组,对应着n个桶,0~99
  const buckets = Array.from(new Array(n), () => {
    return new Array();
  });
  for (const x of li) {
    // 判断x在几号桶里,把1w放到99号桶里
    let i = Math.min(parseInt(x / parseInt(maxNum / n)), parseInt(n - 1));
    buckets[i].push(x); //将x放入到应该去的桶之中
    // 在桶中调整位置,冒泡
    for (let j = buckets[i].length - 1; j >= 1; j--) {
      if (buckets[i][j] < buckets[i][j - 1]) {
        [buckets[i][j], buckets[i][j - 1]] = [buckets[i][j - 1], buckets[i][j]];
      } else {
        break;
      }
    }
  }
  return buckets.flat(Infinity); //扁平
}
const li = Array.from(Array(5000), (v, k) => k).sort((a, b) => Math.random() - Math.random());
console.time();
console.log(bucketSort(li));
console.timeEnd();

基数排序

function radixSort(arr) {
  let buckets = new Array(10); // 定义二维数组buckets,每个桶就是一个一维数组
  for (let i = 0; i < buckets.length; i++) {
    buckets[i] = new Array(arr.length); //防止数据溢出,将每个桶的容量设置为极限的arr.length(空间换时间)
  }
  let buckeElementCounts = new Array(10).fill(0); //一维数组记录每个桶中存放数据的个数
  let max = Math.max(...arr); //得到数组中最大值
  let maxLength = (max + '').length; //得到最大值的位数
  for (let i = 0, n = 1; i < maxLength; i++, n = n * 10) {
    // 每一轮,对每个元素的各个位数进行排序处理
    // 第一次是个位数,第二次是十位数,第三次是百位数
    for (let j = 0; j < arr.length; j++) {
      //取出每个元素的各位的值
      let digitOfElement = Math.floor(arr[j] / n) % 10;
      buckets[digitOfElement][buckeElementCounts[digitOfElement]] = arr[j];
      buckeElementCounts[digitOfElement]++;
    }
    let index = 0;
    for (let k = 0; k < buckeElementCounts.length; k++) {
      // 如果桶中有数据,才放入原数组
      if (buckeElementCounts[k] !== 0) {
        // 循环该桶即第k个桶,即第k个一维数组,放入
        for (let l = 0; l < buckeElementCounts[k]; l++) {
          //取出元素放入arr
          arr[index] = buckets[k][l];
          //arr下标后移
          index++;
        }
        // 每轮处理后,下标要清0
        buckeElementCounts[k] = 0;
      }
    }
  }
  //按照这个桶的顺序,以数组的下标依次取出数据,放入原来的数组中
  return arr;
}

// const arr = [123456, 10, 11, 1, 1, 1, 1, 2, 4, 3, 5, 6, 8, 7, 9];
const arr = Array.from(Array(5000), (v, k) => k).sort((a, b) => Math.random() - Math.random());
console.time();
console.log(radixSort(arr));
console.timeEnd();