数据结构与算法

232 阅读8分钟

算法概述

时间复杂度

空间复杂度

数据结构

基础数据结构

数组

队列和栈

链表

集合

字典和散列表

排序算法

参考:十大经典排序算法总结(JavaScript描述)

1.冒泡排序

数组有多少个元素,就需要遍历多少次数组。然后在每次遍历中,依次拿相邻两个元素的值进行比较,如果前值 > 后值,则交换量值位置,然后继续向后比较,直到数组结束。

冒泡排序的改进在于,减少内循环比较的次数,如:

[3, 2, 5, 4, 1]

在经历两轮冒泡后可以得到:

[2, 3, 1, 4, 5]

此时后两个元素顺序必然已经排序,因为每轮冒泡都会将最大的那个泡泡移动到最后,所以后两个元素就不用参与内循环了,因此可将内循环的次数 - 冒泡的轮数,即外循环的次数。

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

2.选择排序

每次找出本轮最小值并放置在第一位,再找出第二小值并放在第二位

function selectSort(arr) {
    let length = arr.length;
    for (let i=0; i<length; i++) {
        let minIndex = i;
        for (let j=i, j<length; j++) {
            if (arr[minIndex] > arr[j]) {
                minIndex = j; // 始终把最小的值得索引赋值给 minIndex
            }
        }
        
        // 判断是否需要交换最小值到前方
        if (minIndex !== i) {
            [arr[i], arr[minIndex]] = [arr[minIndex], arr[i]];
        }
    }
}

3.插入排序

插入排序每次排一个数组项,以此方式构建最后的排序数组。假定第一项已经排序了,接着,它和第二项进行比较,第二项是应该待在原位还是插到第一项之前呢?这样,头两项就已正确排序,接着和第三项比较(它是该插入到第一、第二还是第三的位置呢?),以此类推。

function insertSort(arr) {
    let length = arr.length;
    for(let i=1; i<length; i++) {
        let j=i;
        let temp = arr[j];
        while(j>0 && arr[j-1] > temp){
            arr[j] = arr[j-1]; // 每次将待插入值与其前面所有元素比较,如果前面元素较大,则他们位置依次向后移动,以便给待插入值腾个地方
            j--;
        }
        arr[j] = temp;
    }
}

4.希尔排序

5.归并排序

冒泡、选择、插入三种排序的基本排序算法性能会比较差,一般不会用于实际编码中,而归并排序的复杂度为 O(nlogn),因此可以在实际中使用。

JavaScript 中 Array.sort() 排序函数使用的排序算法,一般是归并排序或快速排序.

归并排序也是用到了分治的思想.

归并排序,将原始数组切分成较小的数组,直到每个数组都只有一个元素,然后再将小数组归并成大数组,直到最后只有一个排序完毕的大数组.

function mergeSort(arr) {
    let length = arr.length;
    
    if (length === 0) {
        return arr;
    }
    
    index = Math.floor(length / 2);
    let leftArray = arr.slice(0, index);
    let rightArray = arr.slice(index, length);
    
    merge(mergeSort(leftArray), mergeSort(rightArray));
}

function merge(left, right) {
    let il = 0;
    let ir = 0;
    let array = [];
    
    while(il < left.length && ir < right.length) {
        if (left[il] < right[ir]) {
            array.push(left[il++]);
        } else {
            array.push(right[ir++]);
        }
    }
        
    while(il < left.length) {
        array.push(left[il++]);
    }
    while(ir < right.length) {
        array.push(right[ir++]);
    }
    
    return array;
}

6.快速排序

快速排序也属于交换排序,通过元素之间的比较和交换位置来达到排序的目的。

快速排序是设定基准值,并在每次循环事将小于基准值的元素移动到基准值左边,大于基准值的元素移动到基准值右边,从而把数组拆分成两个部分。

这种思路叫做 分治法

(1) 首先,从数组中选择中间一项作为主元。

(2) 然后,创建两个指针,左指针指向数组第一项,右指针指向数组末尾项;

移动左指针,直到该指针值大于等于主元时,接着移动右指针,直到找该指针值小于等于主元;

然后交换两指针的值,并将左右指针 ++/--;

重复(2)这个过程,直到左指针超过了右指针。

这个过程将使得比主元小的值都排在主元之前,而比主元大的值都排在主元之后,这一步叫作划分操作。

(3) 接着,算法对划分后的小数组(较主元小的值组成的子数组,以及较主元大的值组成的子数组)重复之前的两个步骤,直至数组已完全排序。

(function(){
  let arr = [2,8,1,4,7,9,0,3];
  
  quick(arr, 0, arr.length - 1);
  
  console.log('arr:', arr);
})();

// 排序函数
function quick(arr, left, right) {
  let index;
  
  if (arr.length > 1){
    index = partition(arr, left, right);
    
    if (left < index - 1) {
      quick(arr, left, index - 1);
    }
    if (right > index) {
      quick(arr, index, right);
    }
  }
}

// partition 函数
function partition(arr, left, right) {
  // 选取基准元素
  let p = arr[math.floor((right - left) / 2)];
  
  let i = left;
  let j = right;
  while(i <= j) {
    while(arr[i] < p) {
      i++;
    }
    while(arr[j] > p) {
      j--;
    }
    if (i <= j) {
      [arr[i], arr[j]] = [arr[j], arr[i]];
      i++;
      j--;
    }
  }
  
  return i;
}

7.堆排序

8.计数排序

9.桶排序

10.基数排序

搜索算法

顺序搜索

二分搜索

算法常见模式

递归

分治

  • 二分搜索
  • 大整数乘法
  • Strassen矩阵乘法
  • 棋盘覆盖
  • 合并排序
  • 快速排序
  • 线性时间选择
  • 最接近点对问题
  • 循环赛日程表
  • 汉诺塔

分治法是把一个复杂的问题分成两个或更多的相同或相似的子问题,再把子问题分成更小的子问题……直到最后子问题可以简单的直接求解,原问题的解即子问题的解的合并。

分治法所能解决的问题一般具有以下几个特征:

1)、 该问题的规模缩小到一定的程度就可以容易地解决;

2)、 该问题可以分解为若干个规模较小的相同问题,即该问题具有最优子结构性质;

3)、 利用该问题分解出的子问题的解可以合并为该问题的解;

4)、该问题所分解出的各个子问题是相互独立的,即子问题之间不包含公共的子子问题。

动态规划

  • 背包问题
  • 最长公共子串

动态规划是一种在数学和计算机科学中使用的,用于求解包含重叠子问题的最优化问题的方法。其基本思想是,将原问题分解为相似的子问题,在求解的过程中通过子问题的解求出原问题的解。动态规划的思想是多种算法的基础,被广泛应用于计算机科学和工程领域。

动态规划方法通常用来求解最优化问题,这类问题可以有很多可行解,每个解都有一个值,找到具有最优值的解称为问题的一个最优解,而不是最优解,可能有多个解都达到最优值。

设计动态规划算法的步骤:

1)、刻画一个最优解的结构特征

2)、递归地定义最优解的值

3)、计算最优解的值,通常采用自底向上的方法

4)、利用算出的信息构造一个最优解

动态规划与分治法相似,都是组合子问题的解来解决原问题的解,与分治法的不同在于:分治法的子问题是相互独立存在的,而动态规划应用于子问题重叠的情况。

贪心算法

  • 教室调度问题

  • 背包问题

  • 集合覆盖问题

  • NP 完全问题

  • 贪心算法,是指在对问题求解时,总是做出当前看来是最好的选择。也就是说,不从整体最优上加以考虑,他所做出的的是在某种意义上的局部最优解。

  • 贪心算法并不保证是得到最优解,但在某些问题上贪心算法的解就是最优解。要会判断一个问题能否用贪心算法来计算。

局部最优解,比一定就代表是全局最优解。

背包问题

分为 0-1 背包问题和分数背包问题

0-1 背包问题不能用贪心算法。但是分数背包问题可以用。

回溯算法

回溯法(探索与回溯法)是一种选优搜索法,按选优条件向前搜索,以达到目标。但当探索到某一步时,发现原先选择并不优或达不到目标,就退回一步重新选择,这种走不通就退回再走的技术为回溯法,而满足回溯条件的某个状态的点称为“回溯点”。

其基本思想是,在包含问题的所有解的解空间树中,按照深度优先搜索的策略,从根结点出发深度探索解空间树。当探索到某一结点时,要先判断该结点是否包含问题的解,如果包含,就从该结点出发继续探索下去,如果该结点不包含问题的解,则逐层向其祖先结点回溯。

分支界限法

分枝界限法是一个用途十分广泛的算法,运用这种算法的技巧性很强,不同类型的问题解法也各不相同。

分支定界法的基本思想是对有约束条件的最优化问题的所有可行解(数目有限)空间进行搜索。该算法在具体执行时,把全部可行的解空间不断分割为越来越小的子集(称为分支),并为每个子集内的解的值计算一个下界或上界(称为定界)。在每次分支后,对凡是界限超出已知可行解值那些子集不再做进一步分支,这样,解的许多子集(即搜索树上的许多结点)就可以不予考虑了,从而缩小了搜索范围。这一过程一直进行到找出可行解为止,该可行解的值不大于任何子集的界限。因此这种算法一般可以求得最优解。

常见算法问题

参考:互联网公司最常见的面试算法题有哪些?

如何判断链表有环

最小栈的实现

如何求出最大公约数

如何判断一个数是 2 的整数次幂

无序数组排序后的最大相邻差

如何用栈实现队列

寻找全排列的下一个数

删去 k 个数字后的最小值

如何实现大整数相加

如何求解金矿问题

寻找缺失的整数

最少硬币找零问题

背包问题

最长公共子序列

矩阵链相乘

分数背包问题

判断单词是否是回文

去掉一组整型数组重复的值

统计一个字符串出现最多的字母

不借助临时变量,进行两个整数的交换

找出下列正数组的最大差值(股票最大收益问题)

随机生成指定长度的字符串

翻转字符串

阶乘

二分查找(原数组必须有序,否则不行)

无重复字符的最长子串

有效的括号

删除字符串中的所有相邻重复项