前端算法入门三:5大排序算法&2大搜索&4大算法思想

3,351 阅读5分钟

介绍

此篇属于前端算法入门系列的第三篇,主要介绍数据结构与算法中的的5大排序算法2大搜索算法以及我们刷算法面试题常见的4大算法思想,总结常见的解题思路,让你的刷题事半功倍。

文章主要包含以下内容:

  • 冒泡排序
  • 快速排序
  • 插入排序
  • 归并排序
  • 选择排序
  • 顺序搜索
  • 二分搜素
  • 分而治之
  • 动态规划
  • 贪心算法
  • 回溯算法

一、5大基础排序算法

1.冒泡排序(常考)

原理如下:

  1. 比较相邻的元素。如果第一个比第二个大,就交换他们两个,如果不是相等的就跳过比下面的元素 ,这样依次的循环下去 直到所有的元素都比较完成才结束。
  2. 针对所有的元素重复以上的步骤,除了最后一个。
  3. 持续每次对越来越少的元素重复上面的步骤,直到没有任何一对数字需要比较。
function bubbleSort(arr) {
    const len = arr.length
    if (len <= 1) return

    for (let i = 0; i < len - 1; i++) {
        for (let j = 0; j < len - i - 1; j++) {
            if (arr[j] > arr[j + 1]) {
                const temp = arr[j]
                arr[j] = arr[j + 1]
                arr[j + 1] = temp
            }
        }
    }
}

// 功能测试
const arr = [4, 3, 6, 2, 5, 7, 9, 8, 1]
bubbleSort(arr)
console.log(arr) // [1, 2, 3, 4, 5, 6, 7, 8, 9]

2.快速排序(常考)

通过一趟排序将要排序的数据分割成独立的两部分,其中一部分的所有数据都比另外一部分的所有数据都要小,然后再按此方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行,以此达到整个数据变成有序序列。

/**
 * @description 快速排序
 * @author hovinghuang
 */

/**
 * 快速排序 (splice)
 * @param arr 
 * @returns 
 */
function quickSort1(arr: number[]): number[] {
    const len = arr.length
    if (len === 0) return arr

    const midIndex = Math.floor(len / 2)
    const midValue = arr.splice(midIndex, 1)[0]

    const left: number[] = []
    const right: number[] = []

    // 注意: splice 会修改原数组,所以用 arr.length
    for (let i = 0; i < arr.length; i++) {
        const n = arr[i]
        if (n < midValue) {
            left.push(n)
        } else {
            right.push(n)
        }
    }
    return quickSort1(left).concat([midValue], quickSort1(right))
}

/**
 * 快速排序 (slice)
 * @param arr 
 * @returns 
 */
 function quickSort2(arr: number[]): number[] {
    const len = arr.length
    if (len === 0) return arr

    const midIndex = Math.floor(len / 2)
    const midValue = arr.slice(midIndex, midIndex + 1)[0]

    const left: number[] = []
    const right: number[] = []

    for (let i = 0; i < len; i++) {
        if (i === midIndex) continue
        const n = arr[i]
        if (n < midValue) {
            left.push(n)
        } else {
            right.push(n)
        }
    }
    
    return quickSort2(left).concat([midValue], quickSort2(right))
}

// 功能测试
const testArr3 = [3, 2, 5, 1, 8, 7]
console.info('quickSort2:', quickSort2(testArr3))

3.插入排序

插入排序是一种最简单直观的排序算法,它的工作原理是通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。

function insertionSort(arr) {
    for (let i = 1; i < arr.length; i++) {
        const temp = arr[i];
        let j = i;
        while (j > 0) {
            if (arr[j - 1] > temp) {
                arr[j] = arr[j - 1];
            } else {
                break;
            }
            j--;
        }
        arr[j] = temp;
    }
}

// 功能测试
const arr = [4, 3, 6, 2, 5, 7, 9, 8, 1]
insertionSort(arr)
console.log(arr) // [1, 2, 3, 4, 5, 6, 7, 8, 9]

4.归并排序

分为两步:

  • 分割:将待排序的线性表不断地切分成若干个子表,直到每个子表只包含一个元素,这时,可以认为只包含一个元素的子表是有序表。
  • 归并:将子表两两合并,每合并一次,就会产生一个新的且更长的有序表,重复这一步骤,直到最后只剩下一个子表,这个子表就是排好序的线性表。
function mergeSort(arr) {
    if(arr.length === 1) return arr
    
    let mid = Math.floor(arr.length / 2)
    let left = arr.slice(0, mid)
    let right = arr.slice(mid)
    
    return merge(mergeSort(left), mergeSort(right))
}

function merge(a, b) {
    let res = []
    
    while (a.length && b.length) {
        if (a[0] < b[0]) {
            res.push(a[0])
            a.shift()
        } else {
            res.push(b[0])
            b.shift()
        }
    }
    
    if(a.length){
        res = res.concat(a)
    } else {
        res = res.concat(b)
    }
    
    return res
}

// 功能测试
const arr = [4, 3, 6, 2, 5, 7, 9, 8, 1]
console.log(mergeSort(arr)) // [1, 2, 3, 4, 5, 6, 7, 8, 9]

5.选择排序

其基本思想是:

  • 首先在未排序的数列中找到最小(or最大)元素,然后将其存放到数列的起始位置。
  • 接着,再从剩余未排序的元素中继续寻找最小(or最大)元素,然后放到已排序序列的末尾。
  • 以此类推,直到所有元素均排序完毕。
function selectionSort(arr) {
    for (let i = 0; i < arr.length - 1; i++) {
        let indexMin = i;
        for (let j = i; j < arr.length; j++) {
            if (arr[j] < arr[indexMin]) {
                indexMin = j;
            }
        }
        if (indexMin !== i) {
            const temp = arr[i];
            arr[i] = arr[indexMin];
            arr[indexMin] = temp;
        }
    }
}

// 功能测试
const arr = [4, 3, 6, 2, 5, 7, 9, 8, 1]
selectionSort(arr)
console.log(arr) // [1, 2, 3, 4, 5, 6, 7, 8, 9]

6.顺序搜索

function sequentialSearch(arr, target) {
    for (let i = 0; i < arr.length; i++) {
        if (arr[i] === target) {
            return i;
        }
    }
    return -1;
};
const arr = [4, 3, 6, 2, 5, 7, 9, 8, 1]
console.log(sequentialSearch(arr, 8)) // 7

7.二分搜索

二分搜索,也叫折半搜索,是一种在有序数组中查找特定元素的搜索算法。所以是用二分查找的前提是数组必须是有序的.

/**
 * 凡是有序,必二分
 * 凡是二分,时间复杂度必包含 O(logn)
 * 递归代码思路清晰,非递归性能更好
 * @description 二分查找 (循环)
 * @author hovinghuang
 */

/**
 * 二分查找(循环)
 * @param arr 
 * @param target 
 * @returns 
 */
function binarySearch01(arr: number[], target: number): number {
    const len = arr.length;
    if (len === 0) return -1;

    let startIndex = 0;
    let endIndex = len - 1;

    while (startIndex <= endIndex) {
        const midIndex = Math.floor((startIndex + endIndex) / 2); // 将数字向下舍入到最接近的整数
        const midValue = arr[midIndex];

        if (target < midValue) {
            // 目标值较少,则继续在左侧查找 
            endIndex = midIndex - 1;
        } else if (target > midValue) {
            // 目标值较大,则继续在右侧查找 
            startIndex = midIndex + 1;
        } else {
            return midIndex;
        }
    }
    return -1;
}

/**
 * 二分查找(递归)
 * @param arr 
 * @param target 
 */
function binarySearch02(arr: number[], target: number, startIndex?: number, endIndex?: number): number {
    const length = arr.length
    if (length === 0) return -1

    // 开始和结束的范围
    if (startIndex == null) startIndex = 0
    if (endIndex == null) endIndex = length - 1

    // 如果 start 和 end 相遇则结束
    if (startIndex > endIndex) return -1

    // 中间位置
    const midIndex = Math.floor((startIndex + endIndex) / 2)
    const midValue = arr[midIndex]

    if (target < midValue) {
        // 目标值较小,则继续在左侧查找
        return binarySearch02(arr, target, startIndex, midIndex - 1)
    } else if (target > midValue) {
        // 目标值较大,则继续在右侧查找
        return binarySearch02(arr, target, midIndex + 1, endIndex)
    } else {
        // 相等,返回
        return midIndex
    }

}

// 功能测试
// const testArr = [-20, -10, 30];
// const testTarget = 30;
// console.info(binarySearch02(testArr, testTarget));

// 性能测试
// const testArr = [10, 20, 30, 40, 50, 60, 70, 80, 90, 100, 110, 120];
// const testTarget = 30;
// console.time('binarySearch01')
// for (let i = 0; i < 100 * 10000; i++) {
//     binarySearch01(testArr, testTarget)
// }
// console.timeEnd('binarySearch01')

// console.time('binarySearch02')
// for (let i = 0; i < 100 * 10000; i++) {
//     binarySearch02(testArr, testTarget)
// }
// console.timeEnd('binarySearch02')

二、4大算法思想

1.分而治之

分而治之是算法设计中的一种方法。它将一个问题成多个和原问题相似的小问题,递归解决小问题,再将结果合并以解决原来的问题。

场景一:归并排序

  • 分:把数组从中间一分为二
  • 解:递归的对两个子数组进行归并排序
  • 合:合并有序子数组

场景二:快速排序

  • 分:选基准,按基准把数组分成两个子数组
  • 解:递归的对两个子数组进行快速排序
  • 合:合并两个子数组

leetcode

2.动态规划

动态规划是算法设计中的一种方法。它将一个问题分解成相互重叠的子问题,通过反复求解子问题,来解决原来的问题。

场景一:斐波那契数列

  • 定义子问题:F(n) = F(n - 1) + F(n - 2)
  • 反复执行:从2循环到n,执行上述公式

动态规划和分而治之区别?

  • 区别在于子问题是否独立
  • 动态规划的子问题是重叠
  • 分而治之的子问题是独立

leetcode

3.贪心算法

贪心算法是算法设计中的一种方法。期盼通过每个阶段的局部最优选择,从而达到全局最优,但是结果并不一定是最优的。常见的反面例子如:零钱兑换问题。

leetcode

4.回溯算法

回溯算法是算法设计中的一种方法。回溯算法是一种渐进式寻找并构建问题解决方式的策略。回溯算法会先从一个可能的动作开始解决问题,如果不行,就回溯并选择另一个动作,直到将问题解决。

什么问题适合用回溯算法解决?

  • 有很多路
  • 这些路,有思路,也有出路
  • 通常需要递归来模拟所有的路

leetcode

参考文章

您的点赞和评论是我持续更新的动力,感谢关注。