JavaScript实现常见排序和搜索算法

463 阅读4分钟

大家好,我是忆白。最近复习了一下排序和查找算法,趁着这次机会,把一些常见排序和查找算法整理了一下。

排序

有关排序算法,推荐一个不错的可视化网站:visualgo.net/zh/sorting , 使用这个网站查看排序的动画,可以很形象的了解各种排序算法的执行过程。

冒泡排序

每次比较相邻两个,把大的往后移。

  • 比较所有相邻元素,如果第一个比第二个大,则交换它们
  • 一轮下来,可以保证最后一个数是最大的
  • 执行 n-1 轮,就可以完成排序
  • 时间复杂度: O(n*n)
Array.prototype.bubbleSort = function() {
    for (let i = 0; i < this.length-1; i++) { // 共执行length-1趟
        for (let j = 0; j < this.length - 1 - i; j+=1) { // 每一趟都比较从0开始未排序的元素
            // 因为是j和j+1比,所以j只需要取到倒数第二个未排序的元素即可
            if (this[j] > this[j+1]) {
                const temp = this[j];
                this[j] = this[j+1];
                this[j+1] = temp;
            }
        }
    }
};
const arr = [5, 4, 3, 2, 1];
arr.bubbleSort();
console.log(arr);

选择排序

每次都选出最小的数放在最前面

  • 找到数组中的最小值,选中它并将其放置在第一位
  • 接着找到第二小的值,选中它并将其放置在第二位
  • 依此类推,执行n-1轮
  • 时间复杂度:O(n*n)
/*
    先假设数组第一个没有排序的元素是最小值,然后遍历后续元素,如果比它小,就把那个元素更新为最小值,遍历结束后,使第一位和最小值交换位置;
    第二轮循环从下标为1开始,先假设这位是最小值,继续遍历后续元素,碰到比它小的就更新最小值为那个元素,遍历结束后与下标为1的元素交换;
    第三轮从下标为2开始,以此类推
*/
Array.prototype.selectionSort = function() {
    for (let i=0; i<this.length-1; i++) {
        let indexMin = i; // 先假设最前面第一个没排序的是最小值,保存下标
        for (let j = i; j < this.length; j++) {
            // 遍历后面的,如果有更小的,就更新下标
            if (this[j] < this[indexMin]) {
                indexMin = j;
            }
        }
        // 如果最小值下标不是最开始假设的那个元素,就交换它们
        if (indexMin !== i) { 
            const temp = this[i];
            this[i] = this[indexMin];
            this[indexMin] = temp;
        }
    }
};
​
const arr = [5, 4, 3, 2, 1];
arr.selectionSort();
console.log(arr);

插入排序

  • 从第二个数开始依次取出,往前比较
  • 比它大就把那个数往后移动
  • 否则即找到位置,把之前取出的数插入这个位置
  • 以此类推进行到最后一个数
  • 时间复杂度也是 O(n*n) 但是在排序小型数组时,插入排序比选择和冒泡要好
/*
    首先将第一个元素标记为已排序
    遍历每个没有排序过的元素 X
    提取元素 X
    i = 最后排序过元素的指数 到 0的遍历(依次往前遍历)
    如果现在排序过的数元素 > 提取的元素
    就将排序过的元素后移一位
    否则,找到插入位置,插入提取的元素
*/
Array.prototype.insertionSort = function() {
    for (let i = 1; i < this.length; i++) {
        const temp = this[i]; // 取出第一个未排序的数
        let j = i; // 保存当前取出数的下标,以便向前遍历
        while (j>0) {
            // 如果当前遍历的数比取出的数大,就把这个数往后移动
            if (this[j - 1] > temp) {
                this[j] = this[j-1];
            } else { // 否则,找到位置,退出循环
                break;
            }
            j--;
        }
        // 在找到的位置插入数值
        this[j] = temp;
    }
};
​
const arr = [2, 4, 5, 3, 1];
arr.insertionSort();
console.log(arr);

归并排序

每次都分成左右两半,递归直到只有一个数,那么这个数是有序的,然后左右两个数组依次取出有序放入另一个数组合并成一个大的有序数组。

归并排序思路:

  • 分:把数组劈成两半,再递归地对子数组进行“分”操作,直到分成一个个单独的数。
  • 合:把两个数合并为有序数组,再对有序数组进行合并,直到全部子数组合并为一个完整数组

合并两个有序数组:

  • 新建一个空数组res,用于存放最终排序后的数组
  • 比较两个数组的头部,较小者出队并推入res中
  • 如果两个数组还有值,就重复第二步

时间复杂度:

  • 分的时间复杂度是O(logN)
  • 合的时间复杂度是O(n)
  • 综合时间复杂度:O(n * logN);优于冒泡、选择、插入
Array.prototype.mergeSort = function() {
    // 定义递归函数
    const rec = (arr) => {
        // 直到递归到数组中只有一个值,就返回它,只有一个值的数组一定是有序数组
        if (arr.length === 1) {return arr;} 
        const mid = Math.floor(arr.length / 2); // 找到中间坐标
        const left = arr.slice(0, mid); // 分成左半边
        const right = arr.slice(mid, arr.length); // 分成右半边
        
        const orderLeft = rec(left); // 继续递归拆分成两边
        const orderRight = rec(right); // 继续递归拆分成两边
        
        const res = []; // 创建一个数组用来保存合并后的新的有序数组
        // 当两个有序数组都有值时,使用while循环将它们合并到res数组中
        // 按下面逻辑,合并后的数组也是有序数组(升序)
        while(orderLeft.length || orderRight.length) { 
            // 依次shift()弹出两个数组第一个元素中较小的
            if (orderLeft.length && orderRight.length) {
                res.push( (orderLeft[0] < orderRight[0]) ? orderLeft.shift() : orderRight.shift() ); 
            } else if (orderLeft.length) {
                res.push(orderLeft.shift());
            } else if (orderRight.length) {
                res.push(orderRight.shift());
            }
        }
        return res; // 返回这个合并的有序数组
    }
    
    const res = rec(this); // 调用递归函数,得到最终的排序数组
    res.forEach((n, i) => { this[i] = n; }); // 将最终的排序好的数组复制给原数组,改变原数组
};
​
const arr = [2, 4, 5, 3, 1];
arr.mergeSort();
console.log(arr);

快速排序

  • 分区:从数组中任意选择一个“基准”,所有比基准小的元素放在基准前面,比基准大的元素放在基准的后面
  • 递归:递归地对基准前后的子数组进行分区

时间复杂度:

  • 递归的时间复杂度是O(logN)
  • 分区操作的时间复杂度是O(n)
  • 综合时间复杂度O(n*logN)
Array.prototype.quickSort = function() {
    // 定义递归函数
    const rec = (arr) => {
        // 递归出口,数组只有一个数或为空,直接返回
        if (arr.length <= 1) { return arr; }
        const mid = arr[0]; // 设当前数组第一个元素为基准
        const left = []; // 存放基准左边数组元素(小于基准)
        const right = []; // 存放基准右边数组元素(大于基准)
        // 遍历之后的数
        for (let i = 1; i < arr.length; i++) {
            if (arr[i] < mid) { // 把小于基准的推入left数组
                left.push(arr[i]);
            } else { // 大于等于基准的推入right数组
                right.push(arr[i]);
            }
        }
        return [...rec(left), mid, ...rec(right)]; // return排序好的数组
    };
    // 调用递归函数
    const res = rec(this);
    res.forEach((n, i) => { this[i] = n }); // 将排序好的新数组复制给当前调用的数组
};
​
const arr = [2, 4, 5, 3, 1];
arr.quickSort();
console.log(arr);

查找

顺序查找

顺序搜索的思路:

  • 遍历数组
  • 找到跟目标值相等的元素,就返回它的下标
  • 遍历结束后,如果没有搜索到目标值,就返回 -1

时间复杂度:

  • 遍历数组是一个循环操作
  • 时间复杂度:O(n)
Array.prototype.sequentialSearch = function(item) {
    for (let i=0; i<this.length; i++) {
        if (this[i] === item) {
            return i;
        }js
    }
    reutrn -1;
};
​
const res = [1,2,3,4,5].sequentialSearch(3);
console.log(res); // 2

二分查找

  • 二分查找的前提:数组是有序的
  • 从数组的中间元素开始,如果中间元素正好是目标值,则搜索结束
  • 如果目标值大于或小于中间元素,则在大于或小于中间元素的那一半数组中搜索

时间复杂度:

  • 每一次比较都使搜索范围缩小一半
  • 时间复杂度:O(logN)
Array.prototype.binarySearch = function(item) {
    let low = 0;
    let high = this.length - 1;
    while(low<=high) {
        const mid = Math.floor((low + high) / 2);
        const element = this[mid];
        if (element < item) { // 中间元素小于目标值,说明在右半边
            low = mid + 1;
        } else if (element > item) { // 中间元素大于目标值,说明在左半边
            high = mid - 1;
        } else { // 如果等于目标值
            return mid;
        }
    }
    return -1; // 如果循环完,还是没找到,就返回-1
}
​
const res = [1, 2, 3, 4, 5].binarySearch(3);
console.log(res);