前端算法面试题3--排序、搜索、分治

排序:冒泡排序、快速排序、插入排序...

搜索:二分搜索、顺序搜索...

工具理解:https://visualgo.net/zh

排序

冒泡排序--交换

冒泡排序是一种简单的排序算法,它重复地遍历要排序的列表,比较每对相邻的项,然后交换它们的顺序(如果需要)。遍历列表的工作是重复地进行直到没有更多需要交换的元素,也就是说列表已经排序完成了。
 

function bubbleSort(arr) {
    let len = arr.length;
    for (let i = 0; i < len; i++) {
        for (let j = 0; j < len - 1 - i; j++) {
            if (arr[j] > arr[j + 1]) {
                // 交换arr[j]和arr[j + 1]
                let temp = arr[j];
                arr[j] = arr[j + 1];
                arr[j + 1] = temp;
            }
        }
    }
    return arr;
}

选择排序--取最小/最大值


选择排序是一种简单的排序算法,它的工作原理是每一次从待排序的数据元素中选出最小(或最大)的一个元素,存放在序列的起始位置,直到全部待排序的数据元素排完。
 

function selectionSort(arr) {
    let len = arr.length;
    for (let i = 0; i < len; i++) {
        let minIndex = i;
        for (let j = i + 1; j < len; j++) {
            if (arr[j] < arr[minIndex]) {
                minIndex = j;
            }
        }
        if (minIndex !== i) {
            // 交换 arr[i] 和 arr[minIndex]
            let temp = arr[i];
            arr[i] = arr[minIndex];
            arr[minIndex] = temp;
        }
    }
    return arr;
}

插入排序--打扑克

对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。

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 && arr[j] > key) {
            //已排序的元素大于新元素,将该元素移动到下一个位置
            arr[j + 1] = arr[j];
            j = j - 1;
        }
        arr[j + 1] = key;
    }
    return arr;
}

归并排序--二路归并 2分组

其主要思想是采用分治法,将已有序的子序列合并,得到完全有序的序列。即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为二路归并。

function mergeSort(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);
  
    return merge(mergeSort(left), mergeSort(right));
}
  
// 合并两个已经排序的数组
function merge(left, right) {
    let result = [];
    let i = 0;
    let j = 0;
  
    while (i < left.length && j < right.length) {
        if (left[i] < right[j]) {
            result.push(left[i]);
            i++;
        } else {
            result.push(right[j]);
            j++;
        }
    }
  
    return result.concat(left.slice(i)).concat(right.slice(j));
}

快速排序--基准元素

其基本思想是选择一个基准元素,将列表划分为两个子列表,一个包含比基准元素小的元素,另一个包含比基准元素大的元素。然后对这两个子列表递归地应用快速排序算法。

function quickSort(arr, low = 0, high = arr.length - 1) {
    if (low < high) {
        let pi = partition(arr, low, high);

        quickSort(arr, low, pi - 1);
        quickSort(arr, pi + 1, high);
    }
    return arr;
}

function partition(arr, low, high) {
    let pivot = arr[high];
    let i = (low - 1);

    for (let j = low; j <= high - 1; j++) {
        if (arr[j] < pivot) {
            i++;
            [arr[i], arr[j]] = [arr[j], arr[i]];
        }
    }
    [arr[i + 1], arr[high]] = [arr[high], arr[i + 1]];
    return (i + 1);
}

排序的时间复杂度
 

排序算法的时间复杂度会根据不同的情况和算法类型有所不同。以下是一些常见的排序算法及其不同情况下的时间复杂度:

  1. 快速排序(Quick Sort):

    • 最好情况:O(n log n)
    • 平均情况:O(n log n)
    • 最坏情况:O(n^2)
  2. 归并排序(Merge Sort):

    • 最好情况:O(n log n)
    • 平均情况:O(n log n)
    • 最坏情况:O(n log n)
  3. 堆排序(Heap Sort):

    • 最好情况:O(n log n)
    • 平均情况:O(n log n)
    • 最坏情况:O(n log n)
  4. 冒泡排序(Bubble Sort):

    • 最好情况:O(n)(当输入已经排序)
    • 平均情况:O(n^2)
    • 最坏情况:O(n^2)
  5. 插入排序(Insertion Sort):

    • 最好情况:O(n)(当输入已经排序)
    • 平均情况:O(n^2)
    • 最坏情况:O(n^2)
  6. 选择排序(Selection Sort):

    • 最好情况:O(n^2)
    • 平均情况:O(n^2)
    • 最坏情况:O(n^2)

以上时间复杂度都是在考虑元素比较次数的前提下得出的。在实际应用中,还需要考虑其他因素,如数据的初始顺序、算法的空间复杂度、元素移动次数等,来选择最适合具体情况的排序算法。

搜索

二分搜索-折半搜索

二分搜索(也称为二分查找或折半查找)是一种在有序数组中查找特定元素的搜索算法。它的工作原理是不断将数组分成两半并查找目标元素,直到找到目标元素或搜索范围为空为止。

function binarySearch(arr, target) {
    let low = 0;
    let high = arr.length - 1;

    while (low <= high) {
        let mid = Math.floor((low + high) / 2);

        if (arr[mid] === target) {
            return mid; // 返回目标元素的索引
        } else if (arr[mid] < target) {
            low = mid + 1;
        } else {
            high = mid - 1;
        }
    }

    return -1; // 如果没有找到目标元素,返回-1
}

分治

一个大问题,分成多个小问题,递归解决小问题,将结果合并从而来解决原来的问题

猜数字大小

假设你需要猜测一个在1到n之间的整数(包含1和n),这个数字是预先确定的,我们可以使用二分查找策略(一种分治法的应用)来最小化猜测次数。

以下是使用分治法(二分查找策略)解决猜数字大小问题的基本步骤:

  1. 首先,猜测中间的数字mid = (low + high) / 2,其中low是可能的最小值,high是可能的最大值。

  2. 如果你猜测的数字mid等于目标数字,那么恭喜你,你已经找到了正确的答案,游戏结束。

  3. 如果你猜测的数字mid大于目标数字,那么目标数字一定在low和mid-1之间。因此,你可以将high设为mid - 1,然后回到步骤1。

  4. 如果你猜测的数字mid小于目标数字,那么目标数字一定在mid+1和high之间。因此,你可以将low设为mid + 1,然后回到步骤1。

重复以上步骤,直到找到目标数字为止。

以下是使用JavaScript实现这个策略的代码:

function guessNumber(n, target) {
    let low = 1;
    let high = n;

    while (low <= high) {
        let mid = Math.floor((low + high) / 2);

        if (mid === target) {
            return mid; // 找到目标数字,返回结果
        } else if (mid > target) {
            high = mid - 1;
        } else {
            low = mid + 1;
        }
    }

    return -1; // 如果没有找到目标数字,返回-1
}

多数元素

多数元素是指在一个数组中出现次数大于总数一半的元素。我们可以使用分治法来寻找多数元素,这种方法的基本思想是将原问题分解成若干个规模更小的子问题,然后递归地解决这些子问题,最后合并子问题的解以得到原问题的解。

以下是使用分治法寻找多数元素的基本步骤:

  1. 将数组分为两半。

  2. 对于每一半,找到多数元素。这可以通过递归地调用相同的函数来完成。

  • 现在,我们有两个元素,每个元素都是其各自一半的多数元素。其中一个元素可能是整个数组的多数元素,也可能没有多数元素。为了找出哪一个是多数元素,我们需要在整个数组中计算这两个元素的出现次数。

    var majorityElement=function(nums){
        const countInRang=(start,end,num)=>{
            let count = 0
            for(let i = start;i<=end;i++){
                if(nums[i]=num){
                    count++; v 
                    }
            }
            return count;
        }
        const majority=(start,end)=>{
            if(start == end) return nums[start];
            let mid=start+Math.floor((end-start/2 ));
            //左侧众数
            const l=majority(start,mid);
            //右侧众数
            const r=majority(start+1,mid);
            if(l==r) return l;
                
        }
        return majority(0,nums.length-1)
    }

    动态规划

    动态规划(Dynamic Programming,简称DP)是一种用于求解多阶段决策问题的优化算法。它的主要思想是将大问题分解为小问题进行解决,然后从小问题的解推导出大问题的解。

    动态规划的关键是解决子问题并将结果存储在一个表中,以便在解决后续问题时可以直接使用,而不需要重复计算。这种方法称为“记忆化”。

    动态规划算法通常用于求解具有某种最优性质的问题。例如,最短路径问题、最大子序列和问题、背包问题等等。

    动态规划的步骤通常包括:

    定义子问题:将原问题分解成一系列子问题。

    实现要反复执行来解决子问题的部分。

    识别并求解出边界条件。

    组合子问题的解以得到原问题的解。

    动态规划并不是可以用于解决所有问题的万能工具,它只适合于具有“重叠子问题”和“最优子结构”特性的问题。其中,“重叠子问题”是指在递归过程中反复出现的子问题,而“最优子结构”是指一个问题的最优解包含其子问题的最优解。

  • 贪心算法
     

    贪心算法(Greedy Algorithm)是一种在每一步选择中都采取在当前状态下最好或最优(即最有利)的选择,从而希望导致结果是最好或最优的算法。

    贪心算法在有最优子结构的问题中尤为有效。最优子结构的意思是局部最优解能决定全局最优解。简单地说,问题能够分解成子问题来解决,子问题的最优解能递推到最终问题的最优解。这种问题最适合使用贪心算法,反之,贪心算法并不是在所有问题都能得到最优解。

    一个例子是找零问题:要找给顾客45美分的硬币,我们会从最大的硬币开始,先找给他两个25美分的,再找给他一个10美分的,最后找给他一个5美分的。这样就得到了最少的硬币数量。

    贪心算法可以解决一些最优化问题,如:求图中的最小生成树、求哈夫曼编码等。然而对于工程和生活中的问题,贪心算法一般不能得到我们所要求的答案,因为贪心算法在每一步的选择上都是看眼前,不从整体最优上加以考虑,所以一般用来解决一些最优化问题,如最小生成树、单源最短路径、任务调度问题等。

你可能感兴趣的:(算法)