数据结构算法总结及应用场景

1,026 阅读11分钟

数组、栈、队列

  • 有效括号(栈):

  • 用栈实现队列:

    • 要点:两个栈,一个负责入队列一个负责出队列,出队列的时候需要考虑是不是为空,如果为空则需要把入队列的栈中的元素pop,并且压入出队列的栈中,这样能保证实现队列的先入先出的特性。
    • 实现代码:github.com/coderElijah…
  • 用队列实现栈:

    • 要点:两个队列,其实刚开始做的时候有点儿想当然了,其实原理是两个队列q1,q2其实入栈操作的时候直接q1.offer就可以,但是出栈操作的时候,需要先把q1.poll不停,然后只剩下一个元素,这个元素就是所谓的栈顶元素,把这个元素返回就行,然后还需要把q1和q2互换,这样才能继续下一次pop,这样可以做到就算换了个队列,代码还是一样的。
    • 实现代码:github.com/coderElijah…

链表

  • 反转链表:

  • 判断链表是否有环:

    • 要点:有两种实现方式一种可以借助set的接口,来遍历进行判重,另一种就是借助两个指针,一个指针每次走一步另一个每次走两步,这样快指针总会追上慢指针也就是两个指针指向相同。(第二种方式比较反思维)
    • 实现代码:github.com/coderElijah…
  • 链表中点:

    • 要点:其实这个可以设置两个指针,一个计数变量,第一个指针从头走到尾,边走边计数,完成之后,第二个指针走一半的数量,返回节点就行。还有一种方法就是类似于上面的环,两个指针一起走,一个步长为2,一个步长为1,快指针走完整个链表,慢指针正好到中点。
    • 实现代码:github.com/coderElijah…

递归、回溯

递归代码模版

function fn(n) {
    // 第一步:判断输入或者状态是否非法?
    if (input/state is invalid) {
        return;
    }

    // 第二步:判读递归是否应当结束?
    if (match condition) {
        return some value;
    }

    // 第三步:缩小问题规模
    result1 = fn(n1)
    result2 = fn(n2)
    ...

    // 第四步: 整合结果
    return combine(result1, result2)
}

回溯代码模版

function fn(n) {

    // 第一步:判断输入或者状态是否非法?
    if (input/state is invalid) {
        return;
  }

    // 第二步:判读递归是否应当结束?
    if (match condition) {
        return some value;
  }

    // 遍历所有可能出现的情况
    for (all possible cases) {
  
        // 第三步: 尝试下一步的可能性
        solution.push(case)
        // 递归
        result = fn(m)

        // 第四步:回溯到上一步
        solution.pop(case)
    
    }
    
}

排序、二分查找

  • 归并排序
    • 要点:其实就是标准的递归的代码模版,先拆解问题规模,然后合并,就这样层层处理。
    • 代码实现:github.com/coderElijah…
  • 快速排序:
    • 要点:代码其实也是基于分治递归的方式,但是跟归并不同的是他不需要合并结果,其实就是将每一次递归的第一个作为一个标准,然后将比他小的放到左边,比他大的放到右边,这样就能确定这个节点的整体相对位置,然后左边在继续确定相对位置,右边确定相对位置,这样递归下去整个数组就都能确定自己的相对位置,达到有序。
    • 代码实现:github.com/coderElijah…
  • 冒泡排序:
    • 要点:就是相邻的数值相互比较,然后将大的交换到最后,然后继续两两相邻比较,直到达到有序状态。
    • 代码实现:github.com/coderElijah…
  • 插入排序:
    • 要点:其实就是用当前的元素跟前面已经有序的元素去比较,然后找到自己的位置,比较的过程中,需要先把当前的值保存下来,往前比较的过程需要把比自己大的元素往后移一位,这样才能到最后空出来自己要插入的槽位。
    • 代码实现:github.com/coderElijah…
  • 选择排序:
    • 要点:每次从无序的数组中选择一个最小的,依次加入到有序的列表中
    • 代码实现:github.com/coderElijah…

排序算法的稳定性

排序算法的稳定性其实指的是能不能够多次排序,比如,需求是按照时间先排序,然后再按照金额排序,如果说按照时间已经排序好了,然后按照金额排序的时候相同的金额的实现先后顺序又乱掉了,就不是稳定排序。

插入排序和冒泡排序都是稳定的,因为已经有序的不会去动它,但是选择排序不是,因为选择排序需要交换位置,如果已经按某个条件有序了,使用另一个条件进行排序,其实是会影响先后未知的。

同样的归并排序在代码上可以写成大小一样的数据不进行交换位置,可以实现稳定排序,但是快速排序也是需要交换位置的,是不稳定的排序算法。

跳表

散列表

二叉树

  • 验证平衡二叉树:
    • 要点:利用平衡二叉树中序遍历的序列是有序的特性,有两种方法,(1)遍历整个树,记录下来中序遍历顺序,然后验证顺序是不是递增的。(2)其实可以只对比中序遍历过程中,前一个遍历节点是不是大于后一个,所以只需要记录前一个节点就行,不需要记录所有节点。
    • 代码实现:github.com/coderElijah…
  • 寻找二叉树中p、q的最近公共祖先节点:
    • 要点:这个其实利用了类似于后序遍历的特性,先在左子树找p q节点,然后在右子树找p q节点,如果左子树找到了那就返回左子树的节点,如果右子树找到了就返回右子树节点,如果都找到了,那么当前节点就是最近公共祖先节点。
    • 代码实现:github.com/coderElijah…
  • 二叉树层次遍历:
    • 要点:其实就是利用一个队列,把每一层的节点加入队列,每一次poll出来,比如访问到的节点访问完成后,将该节点的左右子节点加入队列。其中需要注意一点的是,如果想获得当前的层次,就要使用双重循环,在遍历队列中节点的同时,需要获取到队列元素的数量,这样就能获取一层全部的节点数量,然后在内层循环里遍历队列节点,加入子节点,这样加一个变量来计数,就能知道当前的层次。
    • 代码实现:github.com/coderElijah…
  • 二叉树的最大/最小深度:
    • 要点: 其实和层次遍历一样,层次遍历过程中,只需要记录最先左右子节点都为null的的层次是哪一层,就是最小深度,最大深度就是层次遍历结束后,最后一层的level。这是层次遍历广度优先搜索BFS的方法。还有一种就是深度优先搜索,求最大深度的时候比较容易,就是遍历左右子树取两个子树深度的较大的一个加一,最终返回的就是最大深度。求最小深度就是左右子树较小的一个,然后返回加一,就是最小深度。
    • 代码实现:github.com/coderElijah…
      github.com/coderElijah…

红黑树

B+树

堆与堆排序

  • 数据流中第K大元素:
    • 要点:可以利用堆排序,最小堆,来实现输入的时候只保留k个元素,堆中k满了之后,新进入的元素都和堆顶元素比较,比堆顶大的则删除堆顶,并将新元素加入堆进行排序调整,这样返回的堆顶元素就一直是第k大的。
    • 代码实现:github.com/coderElijah…
  • 滑动窗口最大值:
    • 要点:有两种方法,(1)利用堆排序最大堆,堆大小就是滑动窗口的大小,priorityQueue不停的调整堆顶元素,当窗口滑动过去之后,移除i-k的元素。(2)就是利用双端队列,队列左端存放最大值,当元素划出窗口就在队列左端弹出,队列中存储的是元素下标,这样就能根据窗口大小判断,如果队列左端元素小于等于i-k的时候,就可以把下表弹出。新元素进入的时候会对比右端的大小,如果右端元素的大小小于新入元素,那么就会弹出,直到遇到比他大的元素,或者队列为空,让然后从右端把元素入队,这样就能保证最大的元素永远都是队列左端元素。
    • 代码实现:github.com/coderElijah…

深度、广度优先算法

  • 括号生成:
    • 要点:就是类似于回溯算法的dfs,但是有几个限制点,比如n=3,那么左边括号有三个,右边括号有三个,第二个限制点,就是必须在左边已使用数量大于右边括号已使用数量的时候,才能进行继续回溯,其实有点儿剪枝的味道了。
    • 代码实现:github.com/coderElijah…
  • 课程表(拓扑排序):
    • 原理:其实拓扑排序针对的是有向无环图,比如一个有向图,有一个相互依赖的关系指向,那么用一个数组来表示每一个节点的入度,用一个队列来加载入度为0的节点,出队列之后将这些如度为零的节点指向的节点的如度-1,然后把新的如度为0的节点再次加入到队列中,以此往复,就能得到一个依赖的关系排序。其实就是广度优先搜索利用队列的一个思路。
    • 要点:这个题目的要点比拓扑排序多一点,其实大体上利用拓扑排序就可以了,但是需要判断有没有环,如果最后排序结束,队列为空,但是统计入度的数组中不全为0,那么说明有节点还有相互指向,那么说明有环。
    • 代码实现:github.com/coderElijah…
    • 模版代码:
    void sort() {
    Queue<Integer> q = new LinkedList(); // 定义一个队列 q

    // 将所有入度为 0 的顶点加入到队列 q
    for (int v = 0; v < V; v++) {
        if (indegree[v] == 0q.add(v);
    }

    // 循环,直到队列为空
    while (!q.isEmpty()) {
        int v = q.poll();
        // 每次循环中,从队列中取出顶点,即为按照入度数目排序中最小的那个顶点
        print(v);

        // 将跟这个顶点相连的其他顶点的入度减 1,如果发现那个顶点的入度变成了 0,将其加入到队列的末尾
        for (int u = 0; u < adj[v].length; u++) {
            if (--indegree[u] == 0) {
                q.add(u);
            }
        }
    }
}	

字符串匹配(BF、RK、BM、KMP、AC自动机)

Trie树

动态规划

  • 爬楼梯/斐波那契数列:
    • 要点:这个解决方法其实是可以利用递归,但是递归会有重复计算,所以其实可以使用动态规划,dp[i] = dp[i-1]+dp[i-2];
    • 代码实现:github.com/coderElijah…
  • 三角形最小路径和:
    • 要点:其实这个状态定义比较麻烦,因为三角形反着看,上一层的最短路径,取决于下一层的数值,这也就构成了动态转移方程:dp[i][j] = Math.min(dp[i+1][j],dp[i+1][j+1])+triangle[i][j]。其实还可以把这个动态转移的二维数组压缩成一维的,因为只需要记录下一层的状态就可以,不需要记录所有的,所以转移方程可以优化为dp[i] = Math.min(dp[j],dp[j+1])+triangle[i][j]
    • 代码实现:github.com/coderElijah…
  • 乘积最大子数组:
    • 要点:这个动态规划状态定义为:到第i的数字的最大子数组的乘积dp[i],但是乘积有正负,所以一维定义不够,所以dp[i][0]定义为正最大值,dp[i][1]定义为负最大值,因为这个子序列也有可能只有nums[i]这一个元素,所以动态转移方程就变为:dp[i][0] = max{dp[i-1][0]*nums[i],dp[i-1][1]*nums[i],nums[i]}dp[i][1] = min{dp[i-1][0]*nums[i],dp[i-1][1]*nums[i],nums[i]}。因为求的是最大值,当算乘积的时候子序列有可能只有nums[i]自己,所以之前的最大的乘积有可能会被清掉,所以需要一个max变量来记录,所以每次循环之后需要加一个max = max{dp[i][0],max}.
    • 代码实现:github.com/coderElijah…
  • 最大上升子序列:
    • 要点:这个和乘积最大子数组的区别是不需要连续,这样造成一个问题就是当前最大的子序列的长度,其实就是需要判断前面所有的子序列的长度,选出一个最大的,然后还需要判断,当前的nums[i],是不是大于选出来的nums[j]的值,只有大于才能赋值。所以状态定义为,当前i最大上升子序列的长度dp[i],状态转移方程就是dp[i] = if(nums[i]>nums[j]):max{for(j=0~i)dp[j]}+1。也就是需要两重循环才能解决这个问题。
    • 代码实现:github.com/coderElijah…
  • 零钱兑换:
    • 要点:这里需要注意的点还比较多,首先它类似于跳台阶,因为达到目标amount值,有几种方式,比如硬币是【1,2,5】,那么可以使用一个1硬币达到amount,也可以使用一个2硬币达到,也可以使用一个5硬币达到,所以其实dp[i]的定义就可以是这三种方式的最小的值dp[i] = min{dp[i-1],dp[i-2],dp[i-5]}+1,那么如果硬币的数组的随意的,那么只需要内循环遍历这个数组就行了。其中还有两点需要注意,一个是coins[i]<=amount,不然的话加上这枚硬币就超标了,另一个是dp[]中的元素需要初始化一个大值,不然如果是默认值0的话,那么在对比取最小min的时候,永远是默认值0最小了。
    • 代码实现:github.com/coderElijah…
  • 编辑距离
    • 要点:这里定义的状态就是,包含第字符串a的第i个字符,和字符串b的第j个字符,替换的最小的操作次数为dp[i][j],状态转移方程就是 dp[i][j] = min{dp[i-1][j]+1,dp[i][j-1]+1,dp[i-1][j-1]+1} 这个其实指的就是,如果到字符串a的第i个字符字符串b第j个字符,怎么转化过来的,要么字符串a加一个字符,要么字符串b删一个字符,对应的就是dp[i-1][j] dp[i][j-1]这两个状态,还有一个就是替换那表示需要两个字符都替换,这个时候就要对比这两个字符串是不是相等的,如果想等就不用操作就是dp[i-1][j-1],如果需要替换就是dp[i-1][j-1]+1.
    • 代码实现:github.com/coderElijah…

拓展

  • LRU缓存机制:
    • 要点:可以继承LinedHashMap来实现,里面具体实现就是维护了一个双链表和一个哈希表,将哈希的node,重新封装成entry,然后存储到双链表中,如果超过了容量值,就移除双链表尾部元素然后删除哈希表中的元素。
    • 代码实现:github.com/coderElijah…