数据结构与算法学习笔记

一、目录

1.1、数据结构

  • 数组:Array
  • 栈和队列:Stack / Queue
  • 优先队列(堆):Priority Queue(heap)
  • 链表:LinkedList (Single/Double)
  • 树(二叉树、二叉搜索树):Binary Tree
  • Hash表:HashTable
  • 集合:Disjoint Set
  • 单词查找树:Trie
  • 利用hash函数把数据映射到bit数组中:BloomFilter
  • LRU Cache

1.2、算法

  • 经典编程
  • 中序、前序、后序遍历
  • 贪心算法
  • 递归/回溯算法
  • 广度优先查找
  • 深度优先查找
  • 分治法
  • 动态规划
  • 二分查询

二、时间复杂度

  • O(1):常数复杂度
  • O(log n):对数复杂度
for(int i=1; i<n; i=i*2) {
	System.out.println(i);
}
  • O(n):线性时间复杂度
  • O(n^2):平方
  • O(n^3):立方
  • O(2^n):指数
for(int i-1; i<=Math.pow(2,n); i++) {
	System.out.println(i) //Math.pow(底数,几次方)
}
  • O(n!):阶乘
for(int i=1; i<=factorial(n); i++) {
	System.out.println(i); //factorial为阶乘函数
}

时间复杂度:只看最高复杂度。

O(1) < O(log(n)) < O(n) < O(nlog(n)) < O(n^2) < O(2 ^ n) < O(n!)

时间复杂度差一级,对于高并发系统差别非常大,这就是算法与数据结构重要的关键点。

例子:斐波那契数列:1,1,2,3,5,8,13,21,34,…
F(n) = F(n-1) + F(n-2)

public int fib(n) {
	if (n==0 || n==1) {
    	return n;
    }
    return fib(n-1) + fib(n-2);
}

怎样看递归的时间复杂度呢?看n=5时计算多少次,n=10时计算多少次,这样就可以看出时间复杂度了。

以n=6可以看出:
在这里插入图片描述
实际的时间复杂度是O(2^n)
所以递归的时间复杂度非常差,虽然代码行数少。
所以面试时不要写递归算法。

常见算法的时间复杂度:

  • 二分查找:O(log(n))
  • 二叉树遍历:O(n):因为每个节点仅遍历一次
  • 排序查找或排序二维矩阵查找:O(n) ,二维是O(n), 一维就可以是O(log(n))
  • 各类排序(快排、归并排序等):O(nlog(n))

算法:三分学习七分练习:做常见算法但不太熟:如动态规划、搜索、递归等。

去Leetcode上练习。常看dicussion和solution

做题时,要把所有可能的解法都想出来,然后找个最佳的方案(时间、空间复杂度),千万不要想到一个解法就做题。

给出数列[2, 7, 11, 15]; target=9
因为num[0]+num[1]=9; 所以结果返回
[0,1]

机会永远留给有准备的人!

三、数据结构详解

3.1、数组和链表

3.1.1、数组:内存中连续的区域,可根据下标访问数组中的元素。

在这里插入图片描述
0~8是数组下标,可根据下标随意寻址找到内容,查询时间复杂度是O(1)

但数组的insert, delete的时间复杂度是O(n):
在这里插入图片描述

3.1.2、链表:本质上就是一个个的节点集合,然后通过节点上的指针next存储下一个节点的地址。

链表分为单链表和双链表
在这里插入图片描述
链表的插入过程:
在这里插入图片描述
链表的删除过程:
在这里插入图片描述
链表的插入和删除的时间复杂度都是O(1),但查询时间复杂度是O(n)

双链表:
在这里插入图片描述

3.1.3、习题

习题1、单链表反转

输入:1->2->3->4->5->NULL
输出:5->4->3->2->1->NULL

习题2、给定一个链表,反转相邻的两个节点

输入:1->2->3->4
输出:2->1->4->3

写的时候,可以画图辅助

习题3、给定一个链表,看其中是否有环

思路1:把每个走过的节点放到set, 直接判重。时间复杂度O(n)
思路2:快慢指针:快指针每次走2步;慢指针每次走1步。然后看快慢指针是否相遇。时间复杂度也是O(n), 空间复杂度比思路1更好。

3.2、栈和队列

3.2.1、概念

栈Stack:先入后出:数组或链表
队列queue:先进先出:数组或双向链表

在这里插入图片描述

3.2.2、习题

习题1、给定一个只包含大、中、小括号的字符串,判断字符串是否有效。

示例:输入“() [] {}", 输出:true
输入"{[)]", 输出:false
输入”((([])))", 输出:true

结题思路:使用栈:
左:push; 右:peek
左括号放到stack, 右括号跟栈顶元素看是否匹配。

时间复杂度是O(n),空间复杂度也是O(n)

习题2、只用stack实现queue, 只用queue实现stack

思路:负负得正。
1、用Stack实现queue:
要用两个stack:一个输入stack, 一个输出stack
输入stack:1->2->3
输出栈: 3->2->1
这样输出栈就实现了先入先出了。

3.3、优先队列(堆)

3.3.1、概念

Priority Queue:正常如,按优先级出。

3.3.2、实现机制

1、Heap (Binary, Binomial, Fibonacci)
2、Binary Search Tree

在这里插入图片描述
最小堆:最小的元素在上面,小于左孩子和右孩子。

在这里插入图片描述
最大堆:最大的元素在最上面,大于左孩子和右孩子。

各种堆的时间复杂度:
在这里插入图片描述

3.3.3、习题

3.3.3.1、实时判断数据流中第K大元素

示例:int k=3
int[] arr = [4,5,8,2]

思路1:保持前k个最大值,时间复杂度:N* klog(k)
思路2:最小堆:size=k,时间复杂度:N * log以2为底K的对数

Java中的最小堆,默认就是PriorityQueue:

Class KthLargest {
	final PriorityQueue<Integer> q;
	final int k;
	public KthLargest(int k, int[] a) {
		this.k=k;
		q = new PriorityQueue<>(k);
		for (int n : a) {
			add(n);
		}
	}

	public int add(int n) {
		if (q.size() < k) {
			q.offer(n);
		} else if (q.peek() < n) {
			q.poll();
			q.offer(n);
		}
		return q.peek();
	}
}
3.3.3.2、滑动窗口最大值

示例:输入:nums=[1,3,-1,-3,5,3,6],k=3(k是固定大小的窗口)
输出:[3,3,5,5,6]

思路1:优先队列:大顶堆。时间复杂度是N*log(k)
思路2:因为是固定大小的窗口,直接用双端队列dueue, 每次保留该窗口最大的k个值。时间复杂度O(N)

PriorityQueue: 实际上是一个堆(不指定Comparator时默认为最小堆),通过传入自定义的Comparator函数可以实现大顶堆。
PriorityQueue minHeap = new PriorityQueue(); //小顶堆,默认容量为11

PriorityQueue<Integer> maxHeap = new PriorityQueue<Integer>(11,new Comparator<Integer>(){ //大顶堆,容量11
    @Override
    public int compare(Integer i1,Integer i2){
        return i2-i1;
    }
});

3.4、Map和Set:常用来做查询和计数

题目1、有效的字母异位词,不限字母次序

示例1:
输入 s=“anagram”, t=“nagaram”
输出 true

示例2:
输入 s=“rat”, t=“car”
输出 false

示例3:
输入 s=“rat”, t=“art”
输出 true

思路1:按字母排序,看排序是否相同,时间复杂度O(N*log(N))
思路2:数每个字母出现的次数,用Map计数,时间复杂度是O(N)

题目2、两数之和: 看数组中是否有两个元素的和等于target, 然后返回元素的下标

给定nums=[2,7,11,15], target=9
因为nums[0[ + num[1] = 9, 所以返回 [0, 1]

思路1:暴力求解,时间复杂度O(N^2)
思路2:采用Set存储x,时间复杂度O(n)
然后循环set看9-x是否在set中存在,时间复杂度O(1)
所以时间复杂度是O(n)

题目3、三数之和(高频面试题): 给定一个数组,看是否存在三个数的和等于target

给定数组 nums=[-1, 0, 1, 2, -1, -4],target=0
满足要求的三元组集合为:
[
[-1, 0, 1]
[-1, -1, 2]
]

思路1:暴力求解:三层嵌套循环,时间复杂度O(N^3)
思路2:c=0-(a+b), 看-(a+b)是否在set中存在,时间复杂度是O(N^2)
思路3:先排序,再查找:
先把整数数组排序,
遍历排序后的数组,在遍历的每个元素为a, a左边是b, 最右边是c
判断a+b+c<0, 那么b向右移动1位
a+b+c>0, 那么c向左移动1位

3.5、树、二叉树、二叉搜索树 & 图

树是单链表很像,树只是不只有一个next指针
在这里插入图片描述
完全二叉树:每个节点都有一个左孩子和右孩子。
图:可以从孩子指回父节点(面试不常考图)

链表、树、图的关系:
链表是特殊化的树;树是特殊化的图。

3.5.1、二叉搜索树

也叫有序二叉树,指一颗空树或具备如下特征的二叉树:
1、左子树上所有节点的值均小于它根节点的值
2、右子树上所有节点的值均大于他的根节点的值
3、递归:左右子树也分别为二叉查找树

二叉树用的最多的场景就是二叉搜索树。
在这里插入图片描述
这样每次查找,根据与根的大小比较,可以减少一半的查找量,把时间复杂度从O(N)变成了O(log(n))

实战中很多二叉搜索树都是用红黑树来实现的。

3.5.2、习题

题目1、验证二叉搜索树

示例1:
输入:[3,1,5,null, null, 2]
输出:true

示例2:
输入:[5,1,4,null, null, 3,6]
输出:false

在这里插入图片描述
思路1:中序遍历(中序过程中,看根节点是否大于左节点,否则return false),看结果是否与给定序列相同。时间复杂度O(N)
思路2:递归:左子树找最大值max,右子树找最小值min
然后判断max<root, 且min>root。时间复杂度O(N)

中序遍历:左、根、右。
前序遍历:根、左、右。
后续遍历:左、右、根。

题目2、二叉树/二叉搜索树两个节点的最近公共祖先

示例1:
输入: root=[6,2,8,0,4,7,9,null,null,3,5], p=2, q=8
输出:6

示例2:
输入:root=[6,2,8,0,4,7,9,null,null,3,5], p=2, q=4
输出:2

在这里插入图片描述
思路1:往上寻找,寻找最早重合的路径,时间复杂度O(N)
思路2:递归:findPorQ(root, p, q): 在root为根的子树中找p和q

3.6、二叉树遍历

前序遍历:中、左、右:
A,B,D,E,C,F,G
在这里插入图片描述
中序遍历:左、中、右
D,B,E,A,F,C, G

后续遍历:左、右、根
D,E,B,F,G,C,A

3.7、递归&分治

递归:自己调自己,但要防止死循环。递归可以加个level参数。

题目:求 n! = 1* 2* 3*… * n

public int factorial(int n) {
	if (n <=1) {
		return 1;
	}
	return n * factorial(n-1);
}

在这里插入图片描述
分治:把大问题拆分成n个子问题分别求解
在这里插入图片描述
题目:给定一个字符串:abcdefghij, 把每个字符变成大写。
解题思路:分治法:并行计算。
在这里插入图片描述
分治的好处是没有重复计算。

3.7.1、题目

题目1、pow(x, n),即x的n次幂

示例1:
输入: 2.00000, 10
输出:1024.00000

示例2:
输入:2.10000, 3
输出:9,26100

示例3:
输入:2.00000, -2
输出:0.25000

思路1:直接调库函数,但面试官不允许
思路2:暴力:循环,时间复杂度O(n)
思路3:分治法,时间复杂度O(logN), 可通过递归算法来实现,也可用非递归方式实现。
在这里插入图片描述

题目2、求众数:count(x) > n/2, x是数组中重复的元素,count(x)是x出现的次数,n是数组元素的个数。

示例1:
输入:[1,3,3,2,3]
输出:3

示例2:
输入:[1,1,1,0,2]
输出:1

思路1:暴力法:枚举所有的x,然后算出count(x), 时间复杂度是O(n^2)
思路2:用Map来计数,一次循环,把元素的个数都放到Map中,然后看Map中谁的count最大,谁就是结果。时间复杂度O(n)
思路3:先把数组排序,然后从左到右遍历,只要重复次数大于n/2, 就返回。时间复杂度是O(N*logN)

3.8、贪心算法

对问题求解时,总是做出当前看起来是最好的选择。

在这里插入图片描述
要凑36元,怎样找到纸币个数最少?
这里正好有这样面值的纸币,所以贪心才行,但如果面值不是20,10,5,1, 就不适合用贪心算法了。

面试时考贪心算法非常少,因为贪心算法使用场景非常少。

适用贪心算法:问题一定能拆分成相同的子问题,子问题的最优解就是全局问题的最优解。

通常来说,用贪心算法不能解的题,可考虑用动态规划法。

贪心算法与动态规划法的不同在于:

  • 贪心算法对于每个子问题的解,不能回退
  • 动态规划会保持以前的运算结果,并根据以前的运算结果对当前做出选择,有回退功能。

3.8.1、习题

题目1、买股票的最佳时机

输入:[7,1,5,3,6,4],输出:7
输入:[1,2,3,4,5], 输出:4
输入:[7,6,3,2], 输出:0

3.9、广度优先搜索

适用场景:在树(图)中找到特定节点。
在这里插入图片描述

3.10、深度优先搜索

在这里插入图片描述
深度优先与广度优先对比:
在这里插入图片描述
深度优先:推荐用递归写法
广度优先:非递归写法

3.10.1、习题

题目1、二叉树逐层遍历

给定二叉树[3,9,20,null,null,15,7]
返回逐层遍历结果:
[
[3],
[9,20],
[15,7]
}
在这里插入图片描述
思路1:广度优先
思路2:深度优先:先建好二维数组,然后深度优先,根据层数填充。
两种思路的时间复杂度都是O(n)

题目2、二叉树的最大和最小深度

在这里插入图片描述
给定二叉树:[3,9,20,null,null, 15, 7, null, 4]
返回他的最大深度4,最小深度2

思路1:递归
思路2:深度优先搜索,时间复杂度O(n)
思路3:广度优先搜索,时间复杂度O(n)

题目3、生成括号

给出n代表生成括号的对数,请你写出一个函数,使其能够生成所有可能的并且有效的括号组合。
例如:给出n=3, 生成结果为:
[
“((()))”,
“(()())”,
“(())()”,
“()(())”,
“()()()”
]
思路1:数学归纳法:先n=1:(), 再n=2:()(),(()), 再n=3,… 但这种方式写代码很复杂。
思路2:深度优先搜索,当n给定,字符串的长度也就给出了即2*n。把所有可能的组合,都列出来,然后判断是否合法。但时间复杂度不好O(2^n)
思路3:在思路2上改进:
1)局部组装出如果不合法,就不继续算了。
2)记录左括号用了几次,右括号用了几次, 虽然时间复杂度也是O(2^n),但比思路好快不少。

3.11、剪枝

在这里插入图片描述
把树中不需要的分支去掉。

3.11.1、习题

题目1、国际象棋:N皇后问题

如何将N个皇后放在N*N的棋盘上,并且使得皇后之间不能互相攻击
在这里插入图片描述
思路1:暴力
思路2:数组:
col[j] = 1
pie[i+j]=1
na[i-j]=1
在剪枝时,把横竖撇捺放在Set里,排除Set里的位置。
也可以用位运算,把横竖撇捺的值置为0

题目2、有效的数独

判断一个99的数独是否有效
只需要根据以下规则,验证已填入的数字是否有效即可。
1、数字1-9在每行只能出现一次
2、数字1-9在每列只能出现一次
3、数字1-9在每一个以粗实线分割的3
3宫内只能出现一次

在这里插入图片描述
比较难,解法略

3.12、二分法查找

二分查找的前提:
1、有序的数组
2、数组存在明确的上下界
3、可以通过索引来访问

所以有序数组适合二分查找,而链表不适合。

3.12.1、习题

题目1、实现一个求解平方根的函数 int sqrt(int x)

计算并返回x的平方根,其中x是非负整数,由于返回类型是整数,结果只保留整数部分,小数部分被舍弃。

思路1:二分法:left=0; right=5; mid=2.5
思路2:牛顿迭代法:也是不断的枚举,求他的切线,用它的根来逼近;再求切线,…

1.13、字典树Trie

搜索中的自动提示,用的就是Trie
在这里插入图片描述
Trie树,又称字典树、单词查找树、键树,是Hash树的变种。
其典型应用是统计和排序大量的字符串(但不限于字符串),所以常被搜索引擎用于文本词频统计。

他的优点是:最大限度地减少无畏的字符串比较,查询效率比Hash表高。
在这里插入图片描述

1.13.1、Trie树的核心思想

空间换时间,利用字符串的公用前缀来降低查询时间的开销,以达到提高性能的目的。

在这里插入图片描述

static final int ALPHABET_SIZE=256;

static class TrieNode {
	TrieNode[] children = new TrieNode[ALPHABET_SIZE];
	boolean isEndOfWord=false;
    TrieNode() {
		isEndOfWord=false;
		for (int i=0; i < ALPHABET_SIZE; i++) {
			children[i]=null;
		}
	}
}

Trie树的基本性质:
1、根节点不包含任何字符,除根节点每个节点只包含一个字符
2、从根节点到某一节点,路径上经过的字符连接起来,为节点对应的字符串
3、每个节点的所有子节点包含的字符都不相同。

1.13.2、习题

习题1、实现一个字典树
Class TrieNode {
	public char val;
	public boolean isWord;
	public TrieNode[] children = new TrieNode[26]; // 只支持小写字符a~z
	public TrieNode() {}
	TrieNode (char c) {
		TrieNode node = new TrieNode();
		node.val = c;
	}
}
public class Trie {
	private TrieNode node;
	public Trie() {
		root = new TireNode();
		root.val = " ";
	}

	public void insert(String word) {
		TrieNode ws = root;
		for (int i = 0; i < word.length(); i++) {
			char c = word.charAt(i);
			// 判断该节点是否存在
			if (ws.children[c - 'a'] == null) {
				// 不存在则新建个节点
				ws.children[c - 'a'] = new TrieNode();
			}
			ws = ws.children[c - 'a']
		}
		ws.isWord = true;
	}

	public boolean search(String word) {
		TrieNode ws = root;
		for (int i = 0; i < word.length; i++) {
			char c = word.charAt(i);
			if (ws.children[c - 'a'] == null) {
				return false;
			}
			ws = ws.children[c - 'a'];
		}
		return ws.isWord;
	}

	public boolean startsWith(String prefix) {
		TrieNode ws = root;
		for (int i = 0; i < prefix.length; i++) {
			char c = prefix.charAt(i);
			if (ws.children[c - 'a'] == null) {
				return false;
			}
			ws = ws.children[c - 'a'];
		}
		return true;
	}
}
习题2、二维网格中的单词搜索问题

输入:候选词word = [“oath”, “pea”, “eat”, “rain”];
board=[
[‘o’, ‘a’, ‘a’, ‘n’],
[‘e’, ‘t’, ‘a’, ‘e’],
[‘i’, ‘h’, ‘k’, ‘r’],
[‘i’, ‘f’, ‘l’, ‘a’]
]; 相邻元素可组成单词
输出:[“eat”, “oath”]

思路1:深度优先搜索
思路2:Trie: 先根据word建立Trie树,然后把board可能出现的情况枚举,去Trie中搜索

3.14、位运算

3.14.1、什么是位运算

程序中所有数在内存中都是二进制形式存储的,位运算就是直接对整数在内存中的二进制进行操作。

例如,and本身就是个逻辑运算符,但整数与整数之间可以进行and运算,例如6的二进制是110,11的二进制是1011,那么6 and 11的结果就是2。它是二进制对应位的逻辑运算的结果。(0表示false, 1表示true, 空位都当0处理)

由于位运算直接对内存数据进行操作,不需要转成十进制,因此处理速度非常快。

在这里插入图片描述
在这里插入图片描述

3.14.2、实战常用的位运算操作

x & 1 == 1 OR == 0 : 判断奇偶(x % 2 == 1)
x = x & (x - 1) : 清零最低位的1
x & -x : 得到最低位的1

在这里插入图片描述

3.14.2、题目:求位1的个数

输入:一个无符号整数
输出:返回其二进制表达式中数字位数为”1“的个数

思路1:x %2 == 1, 则count++, 然后x = x>>1
思路2:x = x & (x - 1)

3.15、动态规划(面试重点)

1、递归 + 记忆化 --> 递推
即先用递归 + 记忆化的方式来做题,最后再思考怎样做成递推的形式。

2、状态的定义:opt[n], dp[n], fib[n]
把状态定义成数组,然后用数组来推导出状态转移方程

3、状态转移方程:opt[n] = best_of (opt(n - 1), opt(n - 2), …)
根据前面的opt最优解得出 状态转移方程opt[n]的解

4、最优子结构

例1、斐波拉切数列
0,1,1,2,3,5,8,13,21,…
递推公式:f[n] = f[n-1] + f[n-2]
在这里插入图片描述
在这里插入图片描述
把计算过的值,存到缓存mem(也有就是记忆化),这样就把原来O(2^n)优化为O(n)
在这里插入图片描述
这样就用递归 + 记忆化做到了递推。

例2、一个人从起点走到终点,要么向右,要么向下,不能回退,实心的格子不能走,那么问从起点到终点有多少种走法?(格子高度为M,宽度为N)
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
状态转移方程:
在这里插入图片描述
每个格子的值等于他下面和右面格子值的和。
看左上角,其下面+右面为10 + 17 = 27,也就是共有27种走法。
时间复杂度是O(M*N)
这就是动态规划的最基础的题目。

总结:所谓动态规划,实际上就是动态递推的过程。
递推,最好不要递归+记忆化,递归+记忆化是比较初级阶段采用的方法。

动态规划 与 回溯算法 与 贪心算法 的区别和联系?
1)回溯(递归):重复计算 --> 不存在最优子结构的问题
2)贪心:永远局部最优 --> 局部最优并不一定能达到全局最优
3)动态规划:记录局部最优子结构/多种记录值 --> 通过缓存局部解,记录局部最优值,通过全面推导出全局最优解。

当贪心算法不是只看眼前利益,就变成了动态规划。

动态规划法很难,可能想不到思路,主要原因是功力还不够:解决方法是把常用的解法记下来,然后多多联系,直到达到看到题就有思路的程度。

3.15.1、习题

习题1、爬楼梯

输入:3
输出:3
解释:有三种方法可以爬到楼顶
方法1:1阶 + 1阶 + 1阶
方法2:1阶 + 2阶
方法3:2阶 + 1阶
在这里插入图片描述
思路1:回溯即递归 + 缓存子结果
f(n) = f(n-1) + f(n-2)
f(0) = f(1) = 1

思路2:动态规划
for i=2–>n:
f[n] = f[n-1] + f[n-2]
时间复杂度O(n)

动态规划思路的关键:
1)DP状态的定义:
f(n):定义为到第n阶的总走法个数

2)列出DP方程
f[n] = f[n-1] + f[n-2], 即斐波拉契数列。

习题2、三角形最小路径和

给定三角形:
[
[2]
[3,4]
[6,5,7]
[4,1,8,3]
]

自顶向下的最小路径和为11。即2+3+5+1=11
在这里插入图片描述
规则是他只能走他相邻的两个6下面的点,如3只能走6或5,不能走7。

思路1:回溯(递归)
思路2:动态规划

动态规划时,可以从下往上定义DP状态,通常是二维数组DP[i,j]
DP[i, j] = min( DP[i + 1, j], DP[i+1, j+1]) + self[i, j]
DP[m-1, j] = self[m-1, j]

3.16、并查集

3.16.1、概念

并查集(union and find):是一种树的数据结构,用于处理一些不交集(Disjoint Sets)的合并及查询问题。

Find:确定元素属于哪一个子集,他可以用来确定两个元素是否属于同一个子集。

Union:将两个子集合并成一个集合。

在这里插入图片描述
生活中的例子:
小弟 --> 老大
帮派识别
两种优化方式

在这里插入图片描述

优化一:
在这里插入图片描述
优化二:
在这里插入图片描述

3.16.2、习题:岛屿的个数

给定一个由‘1’(陆地)和‘0’(水)组成的二维网络,计算岛屿的数量。一个岛屿被水包围,并且他是通过水平方向或垂直方向上相邻的陆地连接而成,你可以假设网络的四个边均被水包围。
示例:
输入:
11000
11000
00100
00011
输出:3

在这里插入图片描述
思路1:染色问题
思路2:并查集

3.17、LRU Cache

学习算法也要把常用的套路背会,即放到自己脑子这个cache里,这样考试时才能快速答处理,把O(N)变成O(1)。

LRU(least recently used):最近最少使用。
使用双向链表实现:如LinkedHashMap。
查询时间复杂度:O(1)
修改时间复杂度:O(1)

在这里插入图片描述

习题:实现一个LRU Cache

思路:用双向链表实现

3.18、布隆过滤器

在高并发系统、分布式系统用的非常多。

布隆过滤器用一个很长的二进制向量,和一个映射函数。
布隆过滤器可以用于检索一个元素是否在一个集合中。

布隆过滤器的优点是时间效率和空间效率都远好于一般的算法,但缺点是有一定的错误识别率和删除困难。

布隆过滤器用于判断一个元素在还是不在一个集合,如果判断不在,那么一定不在;如果判断在,那么不一定在,还需要进一步校验(去内存或数据库中查询)。

布隆过滤器的实现,跟Hash表类似,只是布隆过滤器不是把查询映射到数组的某一个位置,而是散射到一个很长的二进制向量中

在这里插入图片描述
x, y, z是已有元素,w是查询是否存在

在这里插入图片描述
A,E是存在的,查询A, C, B是否存在,可以看到,B映射时,发现B存在,但实际B是不存在的。

布隆过滤器使用案例:
1、比特币:
用布隆过滤器判断是否存在,如果不存在就是不存在,如果存在,再去Redis中查询,降低了Redis的负载。

2、分布式系统(Map-Reduce)
在这里插入图片描述

四、总结

4.1、常用算法模板

递归模板:
1)下递归结束条件2)

在这里插入图片描述

在这里插入图片描述
二分:
在这里插入图片描述
DP模板:
在这里插入图片描述
在这里插入图片描述

4.2、练习与刷题

1、需要刻意练习自己不熟悉的算法和数据结构
2、做过的题目要反复练习
3、告别舒适区

4.3、面试答题四件套

在面试前,对前面的模板套路一定要形成机械记忆和条件反射!!!

1、搞清题目
弄清题目细节、边界条件、可能的极端错误情况

2、把所有可能的解法和面试官沟通一遍:不要把面试官当成监考老师,而是把他当成未来的同事,当成讨论问题。
1)每种解法的时间复杂度、空间复杂度
2)最优解

3、写代码

4、测试用例
1)正常用例
2)极端用例
3)想不到的用例

4.4、回到起点

https://blog.csdn.net/shenjian58/article/details/89850701
斐波那契:最好时间复杂度是O(log(n))
在这里插入图片描述

4.5、经验总结

1、三分学、七分练
2、算法和数据结构是内功修养,重在练习(修行)。

  • 0
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值