从零构建算法核心知识地图,打通你的任督二脉~
- 1、不代表能够"正确"回答每一个算法问题,但是合理的思考方向其实更重要,也是正确完成算法面试的前提。
- 2、算法面试优秀并不意味着技术面试优秀,而技术面试优秀也并不意味着能够拿到 Offer。
- 3、把面试的过程看作是和面试官一起探讨一个问题的解决方案。对于问题的细节和应用场景,可以和面试官沟通。而这种沟通本身也是非常重要的,它暗示着你思考问题的方式。
- 4、如果是非常难的问题,对于你的竞争对手来说,也是难的。关键在于你所表达出的解决问题的思路。甚至通过表达解题思路的方向,得出结论:这个问题的解决方案,应该在哪一个领域,我们可以通过查阅或者进一步学习解决问题。例如:对于一组数据进行排序?思考:这组数据有什么特征?
- 1)、有没有可能包含有大量重复元素?如果有这种可能的话,三路快排是更好的选择。否则使用普通快排即可。
- 2)、是否大部分数据距离它正确的位置很近?是否近乎有序?如果是这样的话,插入排序是更好的选择。
- 3)、是否数据的取值范围非常有限?比如对学生成绩排序,如果是这样的话,计数排序是更好的选择。
- 4)、是否需要稳定的排序?如果是的话,归并排序是更好地选择。
- 5)、是否是使用链表存储的?如果是的话,归并排序是更好地选择,因为快排非常依赖于数组的随机访问。
- 6)、数据的大小是否可以装载到内存里?不能的话,需要使用外排序。
在学习和实践做题之间,要掌握平衡。
- 1)、给定一个有序的数组。
- 2)、设计一个 O(nlogn) 的算法。
- 3)、无需考虑额外的空间。
- 4)、数据规模大概是10000。
- 1)、设计几个简单的测试用例,试验一下。
- 2)、不要忽视暴力解法。暴力解法通常是思考的起点。
- 1)、遍历常见的算法思路。
- 2)、遍历常见的数据结构。
- 3)、空间和时间的交互(哈希表)。
- 1)、极端条件的判断:数组为空?字符串为空?数量为0?指针为 NULL?
- 2)、合理的变量名。
- 3)、注意代码的模块化、复用性。
- 1、询问:题目细节、边界条件、可能的极端错误情况。
- 2、可能的解决方案:1)、时间 & 空间。2)、最优解。
- 3、编码。
- 4、测试用例。
n 表示数据规模,O(f(n)) 表示运行算法所需要执行的指令数,和 f(n) 成正比。
- 1)、O(n^2) 的算法可以处理大约10^4级别的数据。
- 2)、O(n) 的算法可以处理大约10^8级别的数据。
- 3)、O(nlogn) 的算法可以处理大约10^7级别的数据。
- 1)、多开一个辅助的数组:O(n)。
- 2)、多开一个辅助的二维数组:O(n^2)。
- 3)、多开常数空间:O(1)。
- 4)、递归是有空间代价的,递归 n 次,空间复杂度就为 O(n)。
忽略了常数,实际时间 T = c1 * n + c2。
比如说把一个数组中的元素取出来这个操作,很有可能不同的语音基于不同的实现,它实际运行的时间是不等的。 就算转换成机器码,它对应的那个机器码的指令数也有可能是不同的。就算指令数是相同的,同样一个指令在 CPU 的底层,你使用的 CPU 不同,很有可能执行的操作也是不同的。所以,在实际上我们大概能说出来这个 c1 是多少,但是很难准确判断其具体的值。
大O的表示的是渐进时间复杂度,当 n 趋近于无穷大的时候,其算法谁快谁慢。
O(log(n)) 与 O(n) 有着本质的差别。
- 1)、不是有递归的函数就是一定是 O(nlogn) 的。
- 2)、递归中进行一次递归调用:递归函数的时间复杂度为 O(T * depth)。
- 3)、递归中进行多次递归调用:画出递归树,计算调用次数。例如2次递归调用:2^0 + ... 2^n = 2^(n+1) - 1 = O(2^n)
- 4)、主定理:归纳了递归函数所计算时间复杂度的所有情况。
假设 capacity = n,n + 1 次 addLast/removeLast,触发 resize,总共进行 2n + 1 次基本操作平均来看,每次 addLast/removeLast 操作,进行 2 次基本操作均摊计算,时间复杂度为 O(1)。
当反复先后调用 addLast/removeLast 进行操作时,会不断地进行 扩容/缩容,此时时间复杂度为 O(n)出现问题的原因:removeLast 时 resize 过于着急。 解决方案:Lazy (在这里是 Lazy 缩容)等到 size == capacity / 4 时,才将 capacity 减半。
- 待排序的元素需要实现 Java 的 Comparable 接口,该接口有 compareTo() 方法,可以用它来判断两个元素的大小关系。
- 使用辅助函数 less() 和 swap() 来进行比较和交换的操作,使得代码的可读性和可移植性更好。
- 排序算法的成本模型是比较和交换的次数。
算法 | 稳定性 | 时间复杂度 | 空间复杂度 | 备注 |
---|---|---|---|---|
选择排序 | × | N2 | 1 | |
冒泡排序 | √ | N2 | 1 | |
插入排序 | √ | N ~ N2 | 1 | 时间复杂度和初始顺序有关 |
希尔排序 | × | N 的若干倍乘于递增序列的长度 | 1 | 改进版插入排序 |
快速排序 | × | NlogN | logN | |
三向切分快速排序 | × | N ~ NlogN | logN | 适用于有大量重复主键 |
归并排序 | √ | NlogN | N | |
堆排序 | × | NlogN | 1 | 无法利用局部性原理 |
快速排序是最快的通用排序算法,它的内循环的指令很少,而且它还能利用缓存,因为它总是顺序地访问数据。它的运行时间近似为 ~cNlogN,这里的 c比其它线性对数级别的排序算法都要小。
使用三向切分快速排序,实际应用中可能出现的某些分布的输入能够达到线性级别,而其它排序算法仍然需要线性对数时间。
Java 主要排序方法为 java.util.Arrays.sort(),对于原始数据类型使用三向切分的快速排序,对于引用类型使用归并排序。
数组最大的优点:快速查询,动态数组实现:
-
数组最好应用于"索引有语义"的情况(但并非所有有语义的索引都适用于数组,例如身份证号码)
-
我们需要额外处理索引有语义的情况
-
数组的容量:capacity,数组的大小:size,初始为0
-
1、实现基本功能:增删改查、各种判断方法等等
-
2、使用 泛型 让我们的数据结构可以放置 "任何"(不可以是基本数据类型,只能是类对象,好在每一个基本类型都有一个对应的包装类,它们之间可以自动进行装箱和拆箱的操作) 的数据类型
-
3、数组的扩容与缩容,手写时的易错点: - 1、注意数组的下标 - 2、注意 size 与 capacity 的区别
- 实现动态数组
- 二分搜索-1
- 二分搜索-2
- lengthOfLongestSubstring-1
- lengthOfLongestSubstring-2
- lengthOfLongestSubstring-3
- lengthOfLongestSubstring-4
- maxArea
- 合并区间
- 合并两个有序数组
- findKthLargest
- reverseRecursive
- FindNumsAppearOnce
- 数组中出现次数超过一半的数字
- 数组中未出现的最小正整数字
- longestPalindrome
- search旋转排序数组
- trap(接雨水)
- sortColors-1
- sortColors-2
- longestConsecutive
- twoSum-1
- twoSum-2
- twoSum-3
- productExceptSelf
- moveZeroes-1
- moveZeroes-2
- moveZeroes-3
- moveZeroes-4
- reverseVowels
- findAnagrams
- findCircleNNum
- maxAreaOfIsland
队列的基本应用 - 广度优先遍历
栈的应用:
- 1)、无处不在的撤销操作
- 2)、系统栈的调用(操作系统)
- 3)、括号匹配(编译器)
- 使用动态数组实现栈
- isValid
- inorderTraversal-1
- inorderTraversal-2
- preorderTraversal-1
- preorderTraversal-2
- postorderTraversal-1
- postorderTraversal-2
- 最小值栈
- 两个队列实现一个栈
- 表达式求值
链表,在节点间穿针引线。
为什么链表很重要?
-
不同于 动态数组、栈、队列的实现:其底层是依托静态数组,靠 resize 解决固定容量问题,
-
链表是真正的动态数据结构,也是最简单的动态数据结构。
-
能够帮助我们更深入地理解引用(指针)与递归。
-
优势:真正的动态,不需要处理固定容量的问题。
-
逆势:不同于数组其底层的数据是连续分布的,链表底层的数据分布是随机的,紧靠next(pre)指针连接,因此链表相对于数组丧失了随机访问的能力。
数组和链表的区别?
- 数组最好被应用于索引有语义的情况,例如 Students[1]。最大的优势:支持动态查询。
- 链表不能被应用于索引有语义的情况。最大的优势:动态。
链表的时间复杂度:
- 增: O(n)
- 删: O(n)
- 改: O(n)
- 查: O(n)
总结:链表不适合去修改,且只适合 增、删、查 链表头的元素,此时时间复杂度为 O(1)。
- 实现链表
- 链表实现队列
- 链表实现栈
- removeNthFromEnd-1
- removeNthFromEnd-2
- mergeTwoLists
- mergeKLists
- reverseKGroup
- deleteDuplicates
- hasCycle
- detectCycle
- reverseList-1
- reverseList-2
- addTwoNumbers
- FindFirstCommonNode
- 单链表的选择排序
- 链表中倒数第k个节点
- 判断一个链表是否为回文结构点
- addTwoNumbers
- swapPairs
- sortList
- getIntersectionNode
- removeElements
- isPalindrome
- deleteNode
- oddEvenList
什么是哈希函数?
哈希函数:将 "键" 转换为 "索引",每一个 "键" 对应唯一的一个索引。
- 很难保证每一个 "键" 通过哈希函数的转换对应不同的 "索引",因此会产生哈希冲突。
- "键" 通过哈希函数得到的 "索引" 分布越均匀越好。
- 在哈希表(空间换时间)上操作,主要要考虑如何解决哈希冲突。
哈希函数的设计
通用的方法——转换为整形处理:
1、小范围正整数直接使用。
2、小范围负整数进行偏移。(-100100 => 0200)
3、大整数:通常做法——取模,例如对身份证号取模后6位,即取最后6位, 因为倒数第5、6位是表示1~31,所以会导致分布不均匀的情况。 一个简单的解决办法就是模一个素数,例如7. 素数取值表:http://planetmath.org/goodhashtableprimes
4、浮点型:在计算机中都是32位或64位的二进制表示,只不过计算机解析成了浮点数。我们可以直接将二进制转换为整形来处理。
5、字符串:
166 = 1 * 10^2 + 6 * 10^1 + 6 * 10^0
code = c * 26^3 + o * 26^2 + d * 26^1 + e * 26^0
code = c * B^3 + o * B^2 + d * B^1 + e * B^0
hash(code) = (c * B^3 + o * B^2 + d * B^1 + e * B^0) % M(素数)
降低计算量:
hash(code) = ((((c * B) + o) * B + d) * B) + e) % M
避免整形溢出:
hash(code) = ((((c % M) * B + o) % M * B + d) % M * B + e) % M
代码形式:
int hash = 0;
for(int i = 0;i < s.length();i++) {
hash = (hash * B + s.charAt(i)) % M;
}
6、复合类型:也是利用字符串的上述公式,例如 Date:year、month、day。
hash(code) = (((date.year % M) * B + date.month) % M * B + date.day) % M
设计原则:
- 1、一致性:如果 a == b,则 hash(a) == hash(b)
- 2、高效性:计算高效简便
- 3、均匀性:哈希值均匀分布
Java 中 Object 的 hashCode 是根据每一个对象的地址映射成整形。
哈希冲突的处理
1、链地址法(Separate Chaining):
- 去掉 k1 符号:(hashCode(k1) & 0x7fffffff) % M,& 0x7fffffff 表示将 k1 最高位变为0。
- HashMap 本质就是一个 TreeMap 数组。
- HashSet 本质就是一个 TreeSet 数组。
- Java8 之前,每一个位置对应一个链表,Java8 开始,当哈希冲突达到一定程度 (平均来讲,每一个位置存储的元素树多于某一个程度了),每一个位置从链表转成红黑树。
- HashMap 容量的选取非常重要,恰当的容量能够避免数组扩容和尽量减少 hash 冲突。
算法复杂度分析:
总共有 M 个地址,如果放入哈希表的元素为 N。
- 如果地址是链表:O(N/M)
- 如果地址是平衡树:O(log(N/M))
为了实现均摊复杂度 O(1),需要对哈希表进行动态空间处理,即
- 1、平均每个地址承载的元素多过一定程度,即扩容 N/M >= upperTol
- 2、平均每个地址承载的元素少过一定程度,即缩容 N/M < lowerTol
- 3、对于哈希表来说,元素树从 N 增加到 upperTol * N,地址空间翻倍
- 4、每个操作在 O(LOWER_TOL)~O(UPPER_TOL) => O(1)
更复杂的动态空间处理方法
扩容 M -> 2 * M => 不是素数
解决方案:使用素数取值表中的值:http://planetmath.org/goodhashtableprimes
哈希表相比 AVL、红黑树 而言,牺牲了顺序性,换来了更好的性能。
集合、映射:
- 1、有序集合(TreeSet)、有序映射(TreeMap):平衡树。
- 2、无序集合(HashSet)、无序映射(HashMap):哈希表。
更多的哈希冲突处理方法:
1、开发地址法(线性探测法):对于开放地址法来说,每一个地址就是直接存元素, 即每一个地址都对所有的元素是开放的。如果有冲突,直接存到当前索引的下一个索引对应的位置, 如果位置还被占用,继续往下寻找即可。 (平方探测法): +1、+4、+9、+16。 (二次哈希法):使用另外一个 hash 算法来计算出下一个位置要去哪儿,hash2(key)。 负载率(LoaderFactory):元素占总容量比,超过它就需要进行扩容, 一般选取为50%,选取合适时间复杂度可以到达 O(1)。
2、再哈希法(Rehashing):使用另外一个 hash 算法来计算出下一个位置要去哪儿。
3、Coalesced Hashing:综合了 Separate Chaining 和 Open Addressing。
两类查找问题:
- 1、查找有无:元素 a 是否存在?set;集合。
- 2、查找对应关系(键值对应):元素 a 出现了几次?map;字典。
常见操作:
- 1、insert
- 2、find
- 3、erase
- 4、change(map)
- 实现哈希表
- twoSum-1
- twoSum-2
- threeSum
- firstUniqChar
- groupAnagrams
- isIsomorphic
- containsDuplicate
- containsNearbyDuplicate
- containsNearbyAlmostDuplicate
- isAnagram
- intersection-1
- intersection-2
- intersect-1
- intersect-2
- numberOfBoomerangs
- frequencySort
- fourSumCount-1
- fourSumCount-2
树结构本身是一种天然的的组织结构,用树存储数据能更加高效地搜索。
二叉树:和链表一样,动态数据结构。二叉树天然的递归结构,空也是一颗二叉树。
- 1)、对于每一个节点,最多能分成2个节点,即左孩子和右孩子。
- 2)、没有孩子的节点称为叶子节点。
- 3)、每一个孩子节点最多只能有一个父亲节点。
- 4)、二叉树具有天然的递归结构,即每个节点的左右子树都是二叉树。
注意:一个节点也是二叉树、空也是二叉树。
满二叉树:除了叶子节点外,每个节点都有两个子节点。
二分搜索树:
- 1)、二分搜索树是一个二叉树,且其每一颗子树也是二分搜索树。
- 2)、二分搜索树的每个节点的值大于其左子树的所有节点的值,小于其右子树的所有节点的值。
- 3)、存储的元素必须有可比较性。
- 4)、通常来说,二分搜索树不包含重复元素。如果想包含重复元素的话,只需定义: 左子树小于等于节点;或者右子树大于等于节点。注意:数组和链表可以有重复元素。
什么是遍历操作?
- 1)、遍历就是把所有的节点都访问一遍。
- 2)、访问的原因和业务相关。
- 3)、在线性结构下,遍历是极其容易的,但是在树结构中,遍历会稍微有点难度。
如何对二叉树进行遍历?
对于遍历操作,两颗子树都要顾及。
- 前序遍历:最自然和常用的遍历方式。规律:根左右
- 中序遍历:规律:左根右
- 后序遍历:中序遍历的结果就是我们在二叉搜索树中所有数据排序后的结果。规律:左右根。应用:为二分搜索树释放内存。
心算出二叉搜索树的前中后序遍历:每一个二叉树都会被访问三次,从根节点出发,
- 前序遍历:当一个节点被访问第一次就记录它。
- 中序遍历:当一个节点被访问第二次的时候记录它。
- 后序遍历:当一个节点被访问第三次的时候才记录它。
前序遍历的非递归实现(深度优先遍历):需要使用栈记录下一步被访问的元素。
对于二叉搜索树的非递归实现一般有两种写法:
- 1)、经典教科书写法。
- 2)、完全模拟系统调用栈的写法。
层序遍历(广度优先遍历):需要使用队列记录当前出队元素的左右子节点。
广度优先遍历的意义:
- 1)、在于快速地查询要搜索的元素。
- 2)、更快地找到问题的解。
- 3)、常用于算法设计中——无权图最短路径。
- 4)、联想对比图的深度优先遍历与广度优先遍历。
从二分搜索树中删除最小值与最大值:往左走的最后一个节点即是存有最小值的节点,往右走的最后一个节点即是存有最大值的节点。
删除二分搜索树种的任意元素:
- 1)、删除只有左孩子的节点。
- 2)、删除只有右孩子的节点。
- 3)、删除具有左右孩子的节点:
1、找到 s = min(d->right), s 是 d 的后继(successor)节点,也即 d 的右子树中的最小节点。 s->right = delMin(d->right), s->left = d->left, 删除 d,s 是新的子树的根。
2、找到 p = max(d->left), p 是 d 的前驱(predecessor)节点。
如何高效实现 rank(E 是排名第几的元素)? 如何高效实现 select(查找排名第10的元素)?
最好的方式是实现一个维护 size 的二分搜索树:给 Node 节点添加新的成员变量 size。
- 实现二分搜索树
- 实现 AVL 树
- 实现红黑树
- 实现分段树
- 实现前缀树
- 实现并查集
- maxDepth
- lowestCommonAncestor-1
- lowestCommonAncestor-2
- 二叉树根节点到叶子节点的所有路径和
- 二叉树的之字形遍历
- isValidBST
- isSymmetric
- sortedArrayToBST
- isBalanced
- minDepth
- hasPathSum
- rob
- invertTree
- kthSmallest
- binaryTreePaths
- sumOfLeftLeaves
- pathSum
映射 Map
- 1)、存储 Key:value 数据对的数据结构。
- 2)、根据 Key,寻找 Value。
非常容易使用链表或者二分搜索树来实现。
LinkedListMap BSTMap 平均 最差
add、remove、set、get、contains O(n) O(h) O(logn) O(n)
- 普通队列:先进先出,后进后出
- 优先队列:出队顺序和入队顺序无关,和优先级相关
应用场景:操作系统的任务调度,动态 选择优先级最高的任务进行处理。医生和患者之间的关系。
优先队列底层实现 | 入队 | 出队 |
---|---|---|
普通线性结构 | O(1) | O(n) |
顺序线性结构 | O(n) | O(n) |
堆 | O(logn) | O(logn) |
堆的基本结构
二叉堆:满足特殊性质的二叉树。
- 1、二叉堆是一颗完全二叉树,完全二叉树即把元素顺序一层一层地排列成树的形状。
- 2、堆中每一个元素的值都大于等于它的孩子节点。
用数组存储二叉树:
parent = (i - 1) / 2
leftNode = 2 * i + 1
rightNode = 2 * i + 2
递归:本质就是将原来的问题转换为更小的问题。
- 1)、注意递归函数的宏观语义。
- 2)、递归函数就是一个普通的函数,仅完成一个功能而已。
递归算法通常分为两步:
- 1)、求解基本问题。
- 2)、把原问题转化为更小的问题。
递归调用是有代价的:函数调用 + 系统栈空间
其它常见的链表类型:
- 1)、双向链表,每一个 ListNode 同时具有 pre、next 指针
- 2)、双向循环链表:能够更进一步地封装很多便利的操作,Java 中的 LinkedList 的本质就是双向循环链表。
- 3)、数组链表:如果明确知道要存储元素的总数,使用数组链表会更加方便,数组中存储一个个的 Node,Node 包含元素 e 与 int 型的 next 指针。
- permute
- permuteUnique
- removeElements-1
- removeElements-2
- removeElements-3
- removeElements-4
- removeElements-5
- letterCombinations
- generateParenthesis
- combinationSum
- combinationSum2
- solveNQueens
- combine
- combine-2
- subsets
- exist
- subsetsWithDup
- restoreIpAddresses
- floodfill
- partition
- numIslands
- combinationSum3
什么是动态规划?
斐波那契数列——解决递归中的 重叠子问题 && 最优子结构:通过求子问题的最优解,可以获得原问题的最优解:
- 1、记忆化搜索避免重复运算,自上而下的解决问题。
- 2、动态规划,自下而上的解决问题。
动态规划将是将原问题拆解成若干个子问题,同时保存子问题的答案,使得每个子问题只求解一次,最终获得原问题的答案。
- 背包问题
- climbStairs-1
- climbStairs-2
- climbStairs-3
- maxProfit_k_1
- maxProfit_k_inf
- lengthOfLIS-1
- lengthOfLIS-2
- longestCommonSubsequence-1
- longestCommonSubsequence-2
- maxSubArray
- uniquePaths
- minPathSum
- minDistance
- numDecodings
- wordBreak
- rob-1
- rob-2
- rob-3
- rob-4
- rob-5
- rob-6
- rob-7
- rob+
- maximalSquare
- maxInWindows
- numSquare
- maxProfit
- coinChange
- rob
- integerBreak-1
- integerBreak-2
- integerBreak-3
- wiggleMaxLength
- canPartition-1
- canPartition-2
- findTargetSumWays-1
- findTargetSumWays-2
- fib-1
- fib-2
- fib-3
贪心算法: 它也存在最小生成树与最短路径中。
- isSubsequence
- eraseOverlapIntervals-1
- eraseOverlapIntervals-2
- findContentChildren-1
- findContentChildren-2
Bloom Filter 布隆过滤器:
- 1、一个很长的二进制向量和一个映射函数。
- 2、用于检索一个元素是否在一个集合中。
- 3、优点是空间和查询时间效率越超一般算法,缺点是有一定的误识别率(仅当存在时)和删除困难,所以仅仅是一个预先处理模块。
位运算操作:
- 1、X & 1 == 1 OR == 0 判断奇偶(X % 2 == 1)
- 2、X = X & (X-1) => 清零最低位的1
- 3、X & -X => 得到最低位的1。
0s 表示一串 0,1s 表示一串 1。
x ^ 0s = x x & 0s = 0 x | 0s = x
x ^ 1s = ~x x & 1s = x x | 1s = 1s
x ^ x = 0 x & x = x x | x = x
利用 x ^ 1s = ~x 的特点,可以将一个数的位级表示翻转;利用 x ^ x = 0 的特点,可以将三个数中重复的两个数去除,只留下另一个数。
1^1^2 = 2
利用 x & 0s = 0 和 x & 1s = x 的特点,可以实现掩码操作。一个数 num 与 mask:00111100 进行位与操作,只保留 num 中与 mask 的 1 部分相对应的位。
01011011 &
00111100
--------
00011000
利用 x | 0s = x 和 x | 1s = 1s 的特点,可以实现设值操作。一个数 num 与 mask:00111100 进行位或操作,将 num 中与 mask 的 1 部分相对应的位都设置为 1。
01011011 |
00111100
--------
01111111
位与运算技巧
n&(n-1) 去除 n 的位级表示中最低的那一位 1。例如对于二进制表示 01011011,减去 1 得到 01011010,这两个数相与得到 01011010。
01011011 &
01011010
--------
01011010
n&(-n) 得到 n 的位级表示中最低的那一位 1。-n 得到 n 的反码加 1,也就是 -n=~n+1。例如对于二进制表示 10110100,-n 得到 01001100,相与得到 00000100。
10110100 &
01001100
--------
00000100
n-(n&(-n)) 则可以去除 n 的位级表示中最低的那一位 1,和 n&(n-1) 效果一样。
移位运算
>> n 为算术右移,相当于除以 2n,例如 -7 >> 2 = -2。
11111111111111111111111111111001 >> 2
--------
11111111111111111111111111111110
>>> n 为无符号右移,左边会补上 0。例如 -7 >>> 2 = 1073741822。
11111111111111111111111111111001 >>> 2
--------
00111111111111111111111111111111
<< n 为算术左移,相当于乘以 2n。-7 << 2 = -28。
11111111111111111111111111111001 << 2
--------
11111111111111111111111111100100
mask 计算
- 要获取 111111111,将 0 取反即可,~0。
- 要得到只有第 i 位为 1 的 mask,将 1 向左移动 i-1 位即可,1<<(i-1) 。例如 1<<4 得到只有第 5 位为 1 的 mask :00010000。
- 要得到 1 到 i 位为 1 的 mask,(1<<i)-1 即可,例如将 (1<<4)-1 = 00010000-1 = 00001111。
- 要得到 1 到 i 位为 0 的 mask,只需将 1 到 i 位为 1 的 mask 取反,即 ~((1<<i)-1)。
Java 中的位操作
static int Integer.bitCount(); // 统计 1 的数量
static int Integer.highestOneBit(); // 获得最高位
static String toBinaryString(int i); // 转换为二进制表示的字符串
- LRU Cache
- 反转数字
- 生产者消费者
- isPalindrome
- mySqrt
- singleNumber
- majorityElement-1
- majorityElement-2
- isPowerOfTwo-1
- isPowerOfTwo-2
现如今,Android 行业人才已逐渐饱和化,但高级人才依旧很稀缺,我们经常遇到的情况是,100份简历里只有2、3个比较合适的候选人,大部分的人都是疲于业务,没有花时间来好好学习,或是完全不知道学什么来提高自己的技术。对于 Android 开发者来说,尽早建立起一个完整的 Android 知识框架,了解目前大厂高频出现的常考知识点,掌握面试技巧,是一件非常需要重视的事情。
去年,为了进入一线大厂去做更有挑战的事情,拿到更高的薪资,我提前准备了半年的时间,沉淀了一份 「两年磨一剑」 的体系化精品面试题,而后的半年,我都在不断地进行面试,总共面试了二三十家公司,每一场面试完之后,我都将对应的面试题和详细的答案进行了系统化的总结,并更新到了我的面试项目里,现在,在每一个模块之下,我都已经精心整理出了 超高频和高频的常考 知识点。
在我近一年的大厂实战面试复盘中逐渐对原本的内容进行了大幅度的优化,并且新增了很多新的内容。它可以说是一线互联网大厂的面试精华总结,同时后续还会包含如何写简历和面试技巧的内容,能够帮你省时省力地准备面试,大大降低找到一个好工作的难度。
这份面试项目不同于我 Github 上的 Awesome-Android-Interview 面试项目:https://github.com/JsonChao/Awesome-Android-Interview,Awesome-Android-Interview 已经在 2 年前(2020年 10 月停止更新),内容稍显陈旧,里面也有不少点表述不严谨,总体含金量较低。而我今天要分享的这份面试题库,是我在这两年持续总结、细化、沉淀出来的体系化精品面试题,里面很多的核心题答案在面试的压力下,经过了反复的校正与升华,含金量极高。
在分享之前,有一点要注意的是,一定不要将资料泄露出去!细想一下就明白了:
1、如果暴露出去,拿到手的人比你更快掌握,更早进入大厂,拿到高薪,你进大厂的机会就会变小,毕竟现在好公司就那么多,一个萝卜一个坑。
2、两年前我公开分享的简陋版 Awesome-Android-Interview 面试题库现在还在被各个培训机构当做引流资料,加大了现在 Android 内卷。。
所以,这一点一定要切记。
获取方法:扫描下方的二维码。
这是我过去七年一直研究的命题。首先,是为自己研究,因为我是从小城镇出来的,通过持续不断地逆袭立足深圳。越是出身普通的人,就越需要有耐心,去进行系统性地全面提升,这方面,我有非常丰富的实践经验和方法论。因此,我开启了 “JsonChao” 的成长社群,希望和你一起完成系统性地蜕变。
- 每周会提供一份让 个人增值,避免踩坑 的硬干货。
- 每日以文字或语音的形式分享我个人学习和实践中的 思考精华或复盘记录。
- 提供 每月 三 次成长、技术或面试指导的咨询服务。
- 更多服务正在研发中...
- 如果你希望持续提升自己,获得更高的薪资或是想加入大厂,那么超哥的知识星球会对你有很大的帮助。
- 如果你既努力,又焦虑,特别适合加入超哥的知识星球,因为我经历过同样的阶段,而且最后找到了走出焦虑,靠近梦想的地方。
- 如果你希望改变自己的生活状态,欢迎加入超哥的知识星球,和我一起每日迭代,持续精进。
我的公众号 JsonChao
开通啦,专注于构建一套未来Android开发必备的知识体系。每个工作日为您推送高质量文章,让你每天都能涨知识。如果您想第一时间获取最新文章和最新动态,欢迎扫描关注~