03-排序算法的JavaScript实现

1. 排序算法

  • 稳定:如果a原本在b前面,而a=b,排序之后a仍然在b的前面;
  • 不稳定:如果a原本在b的前面,而a=b,排序之后a可能会出现在b的后面。
  • 内排序:所有排序操作都在内存中完成;
  • 外排序:由于数据太大,因此把数据放在磁盘中,而排序通过磁盘和内存的数据传输才能进行。
  • 时间复杂度:一个算法执行所耗费的时间;
  • 空间复杂度:运行完一个程序所需内存的大小。

图片来源于网络

2. 冒泡排序

2-1. 简单的冒泡排序

  1. 比较相邻的元素。如果第一个比第二个大,就交换它们两个;
  2. 对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对,这样在最后的元素应该会是最大的数。
  3. 针对所有的元素重复以上的操作,除了最后一个;
  4. 重复步骤1~3,直到排序完成
function bubbleSort(arr) {
    let length = arr.length;
    for (let i = 0; i < length; i++) {
        for (let j = 0; j < length - 1; j++) {
            if (arr[j] > arr[j + 1]) {
                let tmp = arr[j];
                arr[j] = arr[j + 1];
                arr[j + 1] = tmp;
            }
        }
    }
    return arr;
}

2-2. 改进冒泡排序

  1. 设置一标志性变量post,用于记录每趟排序中最后一次交换的位置。
  2. 由于post位置之后的记录均已交换到位,故在进行下一趟排序时只要扫描到post位置即可。
function bubbleSort2(arr) {
    let i = arr.length - 1;
    while (i) { 
        // 记录当前变化,若无变化则跳出循环
        let pos = 0;
        for (let j = 0; j < i; j++) {
            if (arr[j] > arr[j + 1]) {
                pos = j;
                let tmp = arr[j];
                arr[j] = arr[j + 1];
                arr[j + 1] = tmp;
            }
        }
        i = pos;
    }
    return arr;
}

2-3. 再次改进冒泡排序

  1. 传统冒泡排序中每一趟排序操作只能找到一个最大值或最小值;
  2. 考虑利用每趟排序中进行正向和反向两遍冒泡的方法可以得到两个最终值(最大者和最小者),从而使排序躺数几乎减少了一半。
function swap(arr, a, b) {
  let tmp = arr[a];
  arr[a] = arr[b];
  arr[b] = tmp;
}

function bubbleSort(arr) {
    let low = 0
    let high = arr.length - 1
    while (low < high) {
        for (let i = low; i < high; i++) {
            if (arr[i] > arr[i + 1]) {
                swap(arr, i, i + 1)
            }
        }
        high--
        for (let i = high; i > low; i--) {
            if (arr[i] < arr[i - 1]) {
                swap(arr, i, i - 1)
            }
        }
        low++
    }
}

3. 选择排序

  1. 初始状态:无序区为R[1,…n],有序区为空;
  2. 第i趟排序(i=1,2,3,4,…n-1)开始时,当前有序区和无序区分别为R[1…i-1]和R[i…n]。该趟排序从当前无序区中选出关键字最小的记录R[k],将它与无序区的第1个记录R交换,使R[1,…i]和R[i+1…n]分别变为记录个数增加1个的新有序区和记录个数减少1个的新无序区;
  3. n-1趟结束,数组有序化了。
function swap(arr, a, b) {
  let tmp = arr[a];
  arr[a] = arr[b];
  arr[b] = tmp;
}

function selectionSort(arr) {
  let length = arr.length;
  let min;
  for (let i = 0; i < length; i++) {
    min = i;
    for (let j = i + 1; j < length; j++) {
      if (arr[j] < arr[min]) {
        min = j;
      }
    }
    swap(arr, i, min);
  }
}

4. 插入排序

  1. 从第一个元素开始,该元素可以认为已经被排序;
  2. 取出下一个元素,在已经排序的元素序列中从后向前扫描;
  3. 如果该元素(已排序)大于新元素,将该元素移到下一位置;
  4. 重复步骤3,直到找到已排序的元素小于或等于新元素的位置;
  5. 将新元素插入到该位置后;
  6. 重复步骤2~5.
function insertionSort(arr) {
    let len = arr.length;
    for (let i = 1; i < len; i++) {
        let key = arr[i];
        let j = i - 1;
        while (j >= 0 && key < arr[j]) {
            arr[j + 1] = arr[j];
            j--;
        }
        arr[j + 1] = key;
    }
    return arr;
}

5. 快速排序

快速排序使用分治法来把一个串(list)分为两个子串(sub-list),具体算法描述如下:

  1. 从数列中挑出一个元素,称为“基准”(pivot)
  2. 重新排序数列,所有元素比基准值小的摆放在最前面,所有元素比基准值大的摆在基准的后面(相同的可任一边)。在这个分区退出之后,该基准就处于数列的中间位置。这个称为分区(Partition)操作;
  3. 递归的(recursive)把小于基准值元素的子数列和大于基准值元素的子数列排序。
function quickSort(arr, left, right) {
    if (left >= right) {
        return;
    }
    let i = left;
    let j = right;
    let key = arr[left];
    while (i < j) {
        // 一定要j在前
        // 从右边向左找第一个比key小的数,找到或者两个哨兵相碰,跳出循环
        while (arr[j] > key && i < j) {
            j--;
        }
        // 从左边向右找第一个比key大的数,找到或者两个哨兵相碰,跳出循环
        // 这里的=号保证在本轮循环结束前,key的位置不变,否则的话跳出循环,交换i和from的位置的时候,from位置的上元素有可能不是key
        while (arr[i] <= key && i < j) {
            i++;
        }
        // 代码执行到这里,1、两个哨兵到找到了目标值。2、j哨兵找到了目标值。3、两个哨兵都没找到(key是当前数组最小值)
        if (i < j) {
            let tmp = arr[i];
            arr[i] = arr[j];
            arr[j] = tmp;
        }
    }
    arr[left] = arr[i];
    arr[i] = key;
    quickSort(arr, left, i - 1);
    quickSort(arr, i + 1, right);
}

6. 归并排序

  1. 把长度为n的输入序列分成两个长度为n/2的子序列;
  2. 对这两个子序列分别采用归并排序;
  3. 将两个排序好的子序列合并成一个最终的排序序列
function mergeSort(arr, first, last) {
    if (first < last) {
        const mid = Math.floor((first + last) / 2)
        mergeSort(arr, first, mid)
        mergeSort(arr, mid + 1, last)
        arr = mergeArray(arr, first, mid, last)
    }
    return arr
}
function mergeArray(arr, first, mid, last) {
    let left1 = first
    let right1 = mid
    let left2 = mid + 1
    let right2 = last
    let index = 0
    const temp = new Array()
    while (left1 <= right1 && left2 <= right2) {
        if (arr[left1] < arr[left2]) {
            temp[index++] = arr[left1++]
        } else {
            temp[index++] = arr[left2++]
        }
    }
    while (left1 <= right1) {
        temp[index++] = arr[left1++]
    }
    while (left2 <= right2) {
        temp[index++] = arr[left2++]
    }
    for (let i = 0; i < index; i++) {
        arr[first + i] = temp[i]
    }
    return arr
}
// example
let arr = [10, 3, 1, 5, 11, 2, 0, 6, 3]
let temp = new Array()
let SortedArr = mergeSort(arr, 0, arr.length - 1, temp)
console.log(SortedArr)

7. 堆排序

  1. 将初始二叉树转化为大顶堆(heapify)(实质是从第一个非叶子结点开始,从下至上,从右至左,对每一个非叶子结点做shiftDown操作),此时根结点为最大值,将其与最后一个结点交换
  2. 除开最后一个结点,将其余节点组成的新堆转化为大顶堆(实质上是对根节点做shiftDown操作),此时根结点为次最大值,将其与最后一个结点交换。
  3. 重复步骤2,直到堆中元素个数为1(或其对应数组的长度为1),排序完成。
function swap(Array, i, j) {
    let temp = Array[i]
    Array[i] = Array[j]
    Array[j] = temp
}
function shiftDown(Array, i, length) {
    for (let j = i * 2 + 1; j < length; j = j * 2 + 1) {
        let temp = Array[i]
        if (j + 1 < length && Array[j] < Array[j + 1]) {
            j++
        }
        if (temp < Array[j]) {
            swap(Array, i, j)
            i = j
        } else {
            break
        }
    }
}
function heapSort(Array) {
    // 从第一个非叶子结点开始初始化大顶堆
    for (let i = Math.floor(Array.length / 2 - 1); i >= 0; i--) {
        shiftDown(Array, i, Array.length)
    }
    for (let len = Array.length - 1; len > 0; len--) {
        // 交换根结点和最后一个结点的位置,确定无序序列中的最大值
        swap(Array, 0, len)
        // 从根结点开始继续调整,直到无序序列长度为0
        shiftDown(Array, 0, len)
    }
}

8. 计数排序

  1. 找出待排序的数组中的最大和最小的元素;
  2. 统计数组中每个值为i的元素出现的次数,存入数组C的第i项;
  3. 反向填充目标数组:将每个元素i放在新数组的第C(i)项,每放一个元素就将C(i)减去1
function countSort(arr) {
    let max = arr[0]
    let min = arr[0]
    for (let i = 1; i < arr.length; i++) {
        if (arr[i] > max) {
            max = arr[i]
        } else if (arr[i] < min) {
            min = arr[i]
        }
    }
    /**
     * 因为数组下标均>=0,因此待排序的元素值也必须>=0
     * 为了能让待排序的数也可以是负数
     * 将排序的数的存储位置  = 待排序的数 - min
     */
    let buckets = new Array(max - min + 1)
    for (let i = 0; i < arr.length; i++) {
        let temp = arr[i] - min
        if (buckets[temp]) {
            buckets[temp]++
        } else {
            buckets[temp] = 1
        }
        console.log(buckets)
    }
    let index = 0
    for (let i = 0; i <= buckets.length; i++) {
        let count = buckets[i]
        while (count--) {
            // 还原待排序的数的原始值
            arr[index++] = i + min
        }
    }
}

9. 桶排序

  1. 将要排序的数据分到有限数量的几个有序的桶里
  2. 每个桶里的数据再单独进行排序(一般用插入排序或者快速排序)
  3. 桶内排完序之后,再把每个桶里的数据按照顺序依次取出,组成的序列就是有序的了。
function bucketSort(array, bucketSize) {
    let min = array[0]
    let max = array[0]
    for (let i = 1; i < array.length; i++) {
        if (array[i] > max) {
            max = array[i]
        } else if (array[i] < min) {
            min = array[i]
        }
    }
    // 设置桶的大小默认为5
    const DEFAULT_BUCKET_SIZE = 5
    bucketSize = bucketSize || DEFAULT_BUCKET_SIZE
    // 设置桶的数量,+1是因为存储的桶下标包含0
    const bucketCount = Math.floor((max - min) / bucketSize) + 1
    const buckets = new Array(bucketSize)
    for (let i = 0; i < bucketCount; i++) {
        buckets[i] = []
    }

    // 利用映射函数将数据分配到各个桶中
    for (let i = 0; i < array.length; i++) {
        buckets[Math.floor((array[i] - min) / bucketSize)].push(array[i])
    }
    // 对每个桶内的数据进行排序
    array = []
    for (let i = 0; i < bucketCount; i++) {
        quickSort(buckets[i], 0, buckets[i].length - 1)

        for (let j = 0; j < buckets[i].length; j++) {
            array.push(buckets[i][j])
        }
    }
    return array
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值