迫于打工,开刷算法!
本博客旨在归纳算法题的核心思想并总结对应的模板,从而指导具体的实际问题求解🎺同时感谢labuladong的算法讲解Gitbook访问相关文章。
简单记录一下ACM模式下Python3的数据读取方法。
举个例子,输入n组数据来实现两数之和,输入如下(第一行代表数据组数n,后n行为数据):
3
1 2
3 4
5 6
数据读取方式如下,最后将结果以print
形式输出:
import sys
n = int(sys.stdin.readline())
for i in range(n):
line = sys.stdin.readline().strip()
data = list(map(int, line.split()))
a, b = data
print(a + b)
动态规划问题的一般形式就是求最值,求解动态规划问题的核心是穷举。
动态规划的三要素是重叠子问题、最优子结构、状态转移方程:
整体来说,(回想斐波那契数列求解过程),动态规划的一般流程分三步:暴力的递归解法-->带备忘录的递归解法-->迭代的动态规划解法。
其中带备忘录的递归解法是“自顶向下”的,是将问题不断拆分成子问题的过程;“DP table”的递归解法是“自底向上”的。二者本质相同,都是在以空间换时间的思路进行“聪明的穷举”。
就思考流程来说,分为以下几步:找到状态和选择-->明确dp数组/函数的定义-->确定对每个状态可以做出的选择并择优执行-->确定base case。
其中,状态即为原问题和子问题中变化的变量;base case为初始状态和失败/退出条件。
思路借鉴Leetcode上的一篇英文博文。
现将DP主要归纳为以下五个题型:
for (int j = 1; j <= amount; ++j)
for (int i = 0; i < coins.size(); ++i)
if (coins[i] <= j)
dp[j] = min(dp[j], dp[j - coins[i]] + 1);
routes[i] = routes[i-1] + routes[i-2], ... , + routes[i-k]
for (int i = 1; i < m; ++i)
for (int j = 1; j < n; ++j)
dp[i][j] = dp[i][j-1] + dp[i-1][j];
// i - size of values
// j - options/status of certain value
for (int i = 1; i < n; ++i)
{
// j = 0 : sell the stock
dp[i][0] = max(dp[i-1][0], dp[i-1][1] + prices[i]);
// j = 1 : hold the stock
dp[i][1] = max(dp[i-1][1], dp[i-1][0] - prices[i]);
}
for(int l = 1; l<n; l++) { // l - length
for(int i = 0; i<n-l; i++) { // i - start index
int j = i+l; // j - end index
for(int k = i; k<j; k++) { // k - current index from i to j
dp[i][j] = max(dp[i][j], dp[i][k] + result[k] + dp[k+1][j]);
}
}
}
return dp[0][n-1]
当给定两个字符串,以两个指针从头分别遍历两个字符串:
// i - indexing string s1
// j - indexing string s2
for (int i = 1; i <= n; ++i) {
for (int j = 1; j <= m; ++j) {
if (s1[i-1] == s2[j-1]) {
dp[i][j] = /*code*/; // eg dp[i][j] = dp[i-1][j-1] + 1;
} else {
dp[i][j] = /*code*/; // eg dp[i][j] = max(dp[i-1][j], dp[i][j-1]);
}
}
}
当给定一个字符串,方法如下:
// l - length of sub-string
// i - start index of sub-string
// j - end index of sub-string
for (int l = 1; l < n; ++l) {
for (int i = 0; i < n-l; ++i) {
int j = i + l;
if (s[i] == s[j]) {
dp[i][j] = /*code*/;
} else {
dp[i][j] = /*code*/;
}
}
}
注:此类问题常需要给出O()内的解法。
for 状态1 in 状态1所有取值:
for 状态2 in 状态2所有取值:
for ...
dp[状态1][状态2][...] = 择优(选择1,选择2...)
dp[i] = max(dp[i - 1], nums[i] + dp[i - 2])
。dp[i] = dp[i + 1]
;dp[i] = min{cost(j) + dp[i + j]}
。dp[i - 1, j + 1] = dp[i, j] (if dp[i - 1] == dp[j + 1])
。dp[i][k][0] = max(dp[i - 1][k][0], dp[i - 1][k][1] + prices[i])
;dp[i][k][1] = max(dp[i - 1][k][1], dp[i - 1][k - 1][1] - prices[i])
。dp(i, j) = min(dp(i - 1, j), dp(i, j - 1), dp(i - 1, j - 1)) + 1
。for (j = 0; j < i; j++) {if (nums[i] > nums[j]) dp[i] = max(dp[i], dp[j] + 1);}
。if (text1[i-1] == text2[j-1]) dp[i][j] = dp[i-1][j-1] + 1; else dp[i][j] = max(dp[i-1][j], dp[i][j-1]);
。dp[i][j][0] = max(stones[i] + dp[i+1][j][1], stones[j] + dp[i][j-1][1])// left right
;if (first person chooses left) dp[i][j][1] = dp[i+1][j][0];
;if (first person chooses right) dp[i][j][1] = dp[i][j-1][0];
。for k in range(1, length): # k means stride
for i in range(length - k): # i means x-coordinate
j = i + k # j means y-coordinate
if (s.substr(i, j) in dict && dp[i]) dp[j] = True;
。dp[i] = dp[1]/n + dp[2]/(n-1) ... + dp[i-2]/(n-i+3) + dp[i-1]/(n-i+2)
可推导:dp[i-1] = dp[1]/n + dp[2]/(n-1) ... + dp[i-2]/(n-i+3)
;dp[i] = dp[i-1] + dp[i-1]/(n-i+2)
。dp[i][j] = min(dp[i+1][j], dp[i+1][j+1]) + triangle[i][j];
。dp[i][j] = max(0, min(dp[i+1][j], dp[i][j+1]) - dungeon[i][j])
。for(int k = i; k <= j; k++) {dp[i][j] = max(dp[i][j], dp[i][k-1] + dp[k+1][j] + nums[i-1]*nums[k]*nums[j+1]);}
。(j-i)%(k-1)+1
)合并的最小成本。回溯法的核心思想是将问题分步解决,而在每一步内都尝试所有的可能,当找到解或发现当前路径不可行时,回退到上一步继续尝试其他可能性,dfs则是回溯思想在图(树)上的具体表现形式,在很多情况下可将二者近似等价;由于多数情况下在每一步内的处理方式都是一致的,故递归是dfs的常用实现方式。
dfs可以理解为不撞南墙不回头,即先一条路走到底,不成功则退至最近的、尚有未遍历岔路的路口,选择下一个新岔路再走到底,循环往复。其基本思想是:当扩展的节点存在且未被遍历时,递归执行该节点。由于dfs就是简单地遍历所有情况,故其时间和空间复杂度基本均在O(n)。
(注:回溯法可适用于任何穷举法能解决的问题,DP仅能处理具有最优子结构的问题;回溯法无法大幅优化,只能暴力穷举,DP中存在重叠子问题可以优化;部分DP问题可以通过dfs+记忆化解决;DP一定需要存储子问题的解,回溯法/dfs视情况决定是否要记录当前路径。)
dfs最常用来解决树类问题。由于dfs函数常被递归调用,故常以采用helper辅助函数的形式出现。
Python下利用dfs+递归在树中寻找解的模板如下所示,同时需要思考具体的剪枝方式:
def main(root):
def dfs_helper(root, path):
visited.add(root)
if end condition: # eg:len(path) == target and path not in ans
operation # eg: ans.add(path.copy())
return
for node in root.adj():
if node not in visited:
path.append(node)
dfs_helper(node, path)
path.remove(node)
if not root:
return None
visited = {}
ans = {}
dfs_helper(root, path=[root])
return
在函数的具体写法上,如果path定义在dfs_helper
外,则可以直接对其引用,将dfs_helper
函数简化成def dfs_helper(root)
。
在具体遍历时还需要考虑以下几种情况:图存在重复遍历,树不存在,故图的遍历时需要visited数组记录访问情况,而遍历树时不需要;树的叶节点可能为空,图的节点定存在,故树的遍历时需要判断当前遍历的节点是否为空,而遍历图时不需要;无向图不需要考虑遍历节点的顺序,而树的遍历需要考虑顺序(先遍历左子树还是右子树)。至于更具体的二叉树的前/中/后序遍历,参照我的另一篇博客二叉树的遍历。
在较高的角度审视上述这些代码,DFS的核心思想就是:维护走过的路径,探索当前可执行的选择列表,当触发结束条件时将当前路径记录进集合。而这些加粗的词,在某种程度上分别对应动态规划中的状态、选择和结束条件。可以说,动态规划的暴力求解阶段就是DFS。但因为DP问题具有重叠子问题的性质,通过dp table或备忘录优化,就是对DFS递归树大幅剪枝的过程。
在面对二叉树问题时,我们常常需考虑,能否使用函数在两个子树上的结果整合出当前问题的解,不要盲目冲DFS。最经典题目:树的高度height(root) = max(height(root.left), height(root.right)) + 1
。
在优化时考虑非递归算法,dfs是后入先出结构,故采用栈实现。首先将初始节点压入栈,每个节点出栈时将其标记为已读并将其子节点由右至左压入栈,出栈过程即为输出过程。
遍历图的伪代码可见如下,这次只追求效率,不会再写得很像了🤠:
def dfs(start):
visited = {start}
stack = [start_node]
print(start) # other op
while(stack):
top_of_stack = stack[-1]
for i in iter(top_of_stack.adjacent):
if(!visited[i]):
print(i) # other op
visited[i] = True
stack.append(i)
continue
stack.pop(-1)
遍历二叉树的伪代码如下所示:
def dfs(root):
stack = [root]
while(stack):
top_of_stack = stack.pop(-1)
print(top_of_stack) # other op
# for right_side_node in top_of_stack.child:
# stack.append(right_side_node)
if top_of_stack.right:
stack.append(top_of_stack.right)
if top_of_stack.left:
stack.append(top_of_stack.left)
dfs常用于解决可达性的问题。
bfs可以类比树的层序遍历。
bfs是先入先出结构,采用队列实现。
首先将初始节点压入队列,在节点出队列时进行输出/判断。由左至右地扩展出队列节点的子节点,若未遍历过则将其压入队列,同时标记为已读。
BFS问题的本质在于解决无权图内,起点到终点的的最短路径问题。当第一次遍历到目标节点时,所经过的路径为最短路径。在实现时,需注意使用队列存储节点,也不要忘记对遍历过的节点做标记。
Python伪代码如下所示:注意其中节点弹出、扩展及step累计的语句位置,背就完了。
# calculate the shortest distance from 'root' to 'target'.
def BFS(root, target):
queen = [root]
# use 'visited' to avoid repeat traversal in 'graph', no longer needed in 'tree'.
visited = {root}
step = 0
while queen:
size = len(queen)
for i in range(size):
node = queen.pop(0)
if (node == target):
return step
# expand current node
for x in node.adj():
if x not in visited:
queen.append(x)
visited.add(x)
step = step + 1
C++伪代码如下所示:
// calculate the shortest distance from 'start' to 'target'.
int BFS(Node start, Node target)
{
queue<Node> q{start};
unordered_set<Node> visited{start};
int step = 0;
while(!q.empty())
{
int size = q.size();
for(int i = 0; i < size; i++)
{
Node cur = q.front();
q.pop();
if(cur == target)
return step;
while(cur.adj())
{
Node x = cur.adj();
if(!visited.count(x))
{
q.push(x);
visited.insert(x);
}
}
}
step = step + 1;
}
}
对于已知终点具体值的题型,有一种同量级复杂度下的优化trick:双向BFS。其核心思想是,从起点和终点同时扩展,当二者各自扩展得到的集合有交集时停止。具体在代码实现时,每次仅扩展一个集合,但在每个while循环的开始时,通过判断大小和集合交换,仅对规模相对较小的集合进行扩展,整体上便实现了双向扩展。需要终止条件的判断位置,由于不会对已遍历过的节点进行扩展,故不可在出栈时进行判断,而应在扩展节点时进行终止判断。
Python伪代码如下所示:
# calculate the shortest distance from 'start' to 'target'.
def BFS(start, target):
queen1 = [start]
queen2 = [target]
# use 'visited' to avoid repeat traversal in 'graph', no longer needed in 'tree'.
visited = {start, target}
step = 0
while queen1 and queen2:
if len(queen1) > len(queen2):
queen1, queen2 = queen2, queen1
size = len(queen1)
for i in range(size):
cur = queen1.pop(0)
# expand node 'cur'
for x in cur.adj():
if x in queen2:
return step + 1
if x not in visited:
queen1.append(x)
visited.add(x)
step = step + 1
相较于DFS,BFS在解决最短路径问题时的时间复杂度会低很多(无须遍历所有情况,第一次遍历到正确解时即可返回),但空间复杂度会相对较高。
BFS和DFS两个方法主要是对树、图进行遍历,即从某点出发,对所有顶点进行访问且只访问一次。
一般来说在找最短路径时用BFS,其余时候主要采用DFS。
为回溯,采用邻接矩阵(二维数组)和邻接表对遍历过程进行记录。
我们常将解题的过程融入到出/入 栈/队列的过程中。
二分查找的核心思路就是通过不断收缩左右边界在有序元素中进行高效查找。
二分查找的基本思路如下所示:
low, high = 0, len(nums)
# search in [low, high)
while(low < high):
mid = low + (high - low) // 2
if nums[mid] == target:
return mid
elif nums[mid] < target:
low = mid + 1 # search in [mid + 1, high)
else:
high = mid # search in [low, mid)
return low
C++中的lower_bound()函数和upper_bound()函数则意欲返回第一个大于等于target和第一个大于target的元素索引。也正是因为target可能不存在于数组中,故代码均进行了微小的调整(以下将两份代码写在一起):
low, high = 0, len(nums)
# search in [low, high)
while(low < high):
mid = low + (high - low) / 2
if nums[mid] < target: # lower bound
if nums[mid] <= target: # upper bound
low = mid + 1 # search in [mid + 1, high)
else:
high = mid # search in [low, mid)
return low
树是由具有父子关系的节点构成的常见数据结构。算法题中常基于二叉树进行知识点的考察。
二叉树解题的方法主要分两种,一种是递归,一种是迭代。当题目有较清晰的子结构时可以选择递归。递归调用系统栈,可能占较大空间,但胜在思路清晰;可通过手动维护一个栈/队列的方式将解法优化成迭代方法。当然,部分题目可能会涉及广度/深度优先搜索的知识,需灵活变通。
参考Leetcode官解的一张图:
不难得出:前、中、后序遍历的核心思想均是深度优先搜索,基于栈实现;层序遍历的核心思想是广度优先搜索,基于队列实现。
以下给出树的前、中、后序遍历的递归/迭代写法。注意,所有遍历方法都需谨慎考虑空节点情形。
前序遍历(preorder traversal):对根节点的处理是在处理子节点之前进行的。以二叉树为例,先遍历根节点,再遍历左节点、右节点。
类比实例:遍历目录。其背后的逻辑可以用代码来解释:
def func(Root Node):
print(Root Node)
for each Child Node of Root Node:
func(Child Node)
递归:从左至右遍历所有节点。
def dfs(root):
if root:
print(root) # other op
dfs(root.left)
dfs(root.right)
在此过程中需注意边界条件(叶节点/空节点)的判断和扩展节点的顺序(从左到右)。
迭代:dfs是后入先出结构,故采用栈实现。首先将初始节点压入栈,每个节点出栈时将其标记为已读并将其子节点由右至左压入栈,出栈过程即为输出过程。
def dfs(root):
stack = [root]
while(stack):
node = stack.pop()
print(node) # other op
if node.right:
stack.append(node.right)
if node.left:
stack.append(node.left)
还有一种近似于模板的写法,其思路为:先将根节点和所有的左孩子入栈并输出,每次当前节点为空时,弹出栈顶元素并访问其右孩子,循环操作。
def dfs(root):
cur = root
stack = []
while cur or stack:
if cur:
print(cur) # other op
stack.append(cur)
cur = cur.left
else:
tmp = stack.pop()
cur = tmp.right
中序遍历(inorder traversal):对根节点的处理是在处理子节点之间进行的。以二叉树为例,先遍历左节点,再遍历根节点,最后遍历右节点。
类比实例:表达式树的读取。可以尝试将ab+cde+**
的输入转换为(a+b) * (c*(d+e))
。
递归:
def dfs(root):
if root:
dfs(root.left)
print(root) # other op
dfs(root.right)
迭代:参考上述模板,这次是在出栈时输出:
def dfs(root):
cur = root
stack = []
while cur or stack:
if cur:
stack.append(cur)
cur = cur.left
else:
tmp = stack.pop()
print(tmp) # other op
cur = tmp.right
后序遍历(postorder traversal):对根节点的处理是在处理子节点之后进行的。以二叉树为例,先遍历左节点、右节点,再遍历根节点。
类比实例:统计目录内文件总容量。其背后的逻辑可以用代码来解释:
def size(Root Node):
for each Child Node of Root Node:
total_size += size(Child Node)
total_size += size(Root Node)
递归:
def dfs(root):
if root:
dfs(root.left)
dfs(root.right)
print(root) # other op
迭代:后序遍历的迭代方法较为复杂,一种方法是将前序遍历的根-左-右代码稍加调整,变成根-右-左形式,再倒序输出结果,此方法不够劲,此处不加赘述。此处介绍一种基于辅助数组记忆已遍历节点的方法:
def bfs(root):
stack = [(0, root)] # untraversed
while stack:
flag, node = stack.pop()
if not node:
continue
if flag:
print(node)
else:
stack.append((1, node))
stack.append((0, node.right))
stack.append((0, node.left))
层序遍历(level-order traversal):层序遍历与广度优先搜索方法类似,深度从小到大地逐层处理节点,采用队列进行实现。由于遍历时不存在明显的子结构,故放弃递归思路,用迭代法进行实现。
迭代:迭代法的核心思路是将初始节点压入队列后,在出队列输出该节点的同时,从左到右遍历并将其子节点压入队列。
def bfs(root):
queen = [root]
while queen:
node = queen.pop(0)
print(node) # other op
if node.left:
queen.append(node.left)
if node.right:
queen.append(node.right)
以上部分代码参考博客,在此致谢。
以Leetcode104 二叉树的最大深度为例:
int maxDepth(TreeNode* root) {
if (root == nullptr)
return 0;
return max(maxDepth(root->left), maxDepth(root->right)) + 1;
}
以Leetcode235 二叉搜索树的最近公共祖先为例:
class Solution {
public:
TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) {
//recursive
if ((root->val > p->val) && (root->val > q->val))
return lowestCommonAncestor(root->left, p, q);
if ((root->val < p->val) && (root->val < q->val))
return lowestCommonAncestor(root->right, p, q);
return root;
//iteratively
TreeNode* cur = root;
while(true)
{
if((cur->val > p->val)&&(cur->val > q->val))
cur = cur->left;
else if((cur->val < p->val)&&(cur->val < q->val))
cur = cur->right;
else
break;
}
return cur;
}
};
二叉树:二叉树的平均深度为。
二叉查找树(ADT):对于二叉树中的任一节点X,X的左子树中所有的值均小于X的值,X的右子树中所有的值均大于X的值。易知二叉查找树的平均深度是O(logn),增删查改的时间复杂度也为O(logn)。
AVL树(Adelson-Velskii和Landis):由于二叉查找树在多次插入和删除后可能不平衡,故产生了带有平衡条件的二叉查找树--AVL树。AVL树是每个节点的左子树和右子树的高度最多差1的二叉查找树。实践中,若单次插入使AVL树失衡,通过单旋转/双旋转操作来在O(logN)内平衡树。
当然,二叉树还有很多分类,例如斜树、完全二叉树、满二叉树等。
伸展树(splay tree):二叉查找树和伸展树的单次最坏运行时间均为O(N),但二叉查找树的连续M次最坏运行时间为O(MN),伸展树的连续M次最坏运行时间为O(MlogN),即每次操作的摊还代价是O(logN)。
伸展树的基本想法是,当一个节点被访问后,它就要经过一系列AVL树的旋转放到根上。实际操作中,通过对之字形(zig-zag)和一字形(zig-zig)的不同展开,在将访问节点移动到根处的基础上,将访问路径上大部分节点的深度大致减少一半。
B树:阶为M的B树有如下结构特性:
1 树的根要么为一片树叶,要么其儿子数在2和M之间;
2 除根外,所有非树叶节点的儿子数在和M之间;
3 所有的树叶都在相同的深度上。
所有数据都存储在树叶上,每个内部节点上皆有指向该节点各儿子的指针P1,P2...,Pm和分别代表在子树P2,P3...,Pm中发现的最小关键字的值k1,k2...,km-1。每次find操作时,都从根开始,依据查找的关键字和存储在节点上值来确定当前层在n个方向中的一个方向,直到在最底层找到对应数据。
在插入时若当前树的平衡被破坏(阶为M的B树的某一个节点的儿子数大于M),常通过分裂节点来解决,一些情况下也可以将数据迁移到临近的节点中。同理,删除节点时若破坏平衡,可通过与周围节点进行合并来解决。
对于一次find操作,途径的节点个数为,在路径上的每个节点需要花费O(logM)来确认分支的方向,故find操作的时间复杂度为O(logN)。对于一次插入/删除操作,在某个节点处,最坏时需要O(M)来调整该节点所有信息,整体运算的最坏运行时间为。
B树的最大深度为。从运行时间考虑,M最优选择为3或4;从数据库应用考虑,为了使内部节点装满一个磁盘区块,常有。
数据结构的存储方式只有两种:数组(顺序存储)和链表(链式存储)。数据结构的核心目的是在不同的场景中,尽可能高效地遍历和访问(增删查改)。
二者的特性对比可如下表所示:
实现方法\操作 | Find_Kth | Find_Key | 插入 | 删除 |
---|---|---|---|---|
数组 | 常数时间 | 线性时间 | 线性时间 | 线性时间 |
链表 | 线性时间 | 线性时间 | 线性时间 | 线性时间 |
链表在插入和删除元素时的时间主要花在遍历上。若已经拿到了要删除/插入节点及其前驱节点的信息,时间复杂度骤降到O(1)。链表还有其他形式,例如双链表、循环链表、多重表等。
链表类题目的解法主要有快慢指针法。
栈和队列都隶属于线性表结构。
栈是限制插入和删除只能在一个位置上进行的表,该位置是表的末端,叫做栈的顶端。栈可以理解为后进先出表。
栈可以通过单链表或数组实现。在单链表实现中,表的前端作为栈顶,所有操作均花费常数时间,但该方法的缺点在于对malloc和free的开销昂贵的;更流行的方法是采用数组实现,唯一缺点是需要提前声明一个数组的大小,且错误检测(数组越界)可能影响栈的执行效率。
队列是在末端插入,在开头删除的表。
在采用数组实现队列时,会维护位置变量Front和Rear。为防止队列溢出,只要位置变量达到数组的末端,它就又绕回开头。
优先队列
优先队列是允许至少插入和删除最小者(找出、返回并删除优先队列的最小元素)的数据结构。二叉堆(堆,binary heap)可以在O(logN)内支持以上两种操作。
二叉堆具有结构性和堆序性,每次对堆的操作均需要到堆的所有性质全被满足时方可停止。
结构性:堆满足完全二叉树结构,一个堆数据结构由一个数组、一个代表最大值的整数和当前的堆大小组成。
堆序性:在堆的任意非根节点中,节点的关键字大于其父辈节点的关键字。换而言之,根节点最小。
在插入元素X时,在堆的下一个空闲位置创建一个空穴,若不满足堆结构,将空穴父节点的元素移入空穴,元素上移一层,迭代直至将X合理插入。这种方法称为上滤:新元素在堆中上滤直到找出正确的位置。
散列
散列以常数平均时间进行插入、删除和查找,但不支持任何基于元素间排序的的操作。
散列表数据结构是一个含有关键字的固定大小的数组,通常情况下,关键字是一个带有相关值的字符串(类比Python中的字典结构:关键字key和相关值value)。将每个关键字使用散列函数(Hash function)映射到0至TableSize-1范围中的一个数字,再在数组对应位置存放关键字对应的值。理想情况下,函数在运算简单的同时应尽可能在单元间均匀地分配关键字。
当一个元素准备插入处已存在另一个元素时(二者散列值相同),为消除冲突,常采用分离链接法和开放定址法。
开放定址法的核心思路是在冲突发生时不断选择另外的单元,直到找到空的单元。此方法要求表稍大,装填因子应小于0.5。常用方法有线性探测法,平方探测法和双散列法。
先熟悉一下Python3的常用运算符
运算符 | 功能 | 用例 |
---|---|---|
/ | 除法 | 12 / 5 = 2.4 |
// | 向下取整除 | 5 // 2 = 2; -10 // 3 = -4 |
% | 取模(返回除法余数) | 10 % 3 = 1; 10 % -3 = -2 |
& | 按位与 | 60(0011 1100) & 13(0000 1101) = 12(0000 1100) |
| | 按位或 | 60(0011 1100) | 13(0000 1101) = 61(0011 1101) |
^ | 按位异或 | 60(0011 1100) ^ 13(0000 1101) = 49(0011 0001) |
~ | 按位取反 | ~60(0011 1100) = -61(1100 0011) |
<< | 左移若干位(高位丢弃,低位补0) | 60(0011 1100) << 2 = 240(1111 0000) |
>> | 右移若干位(高位补0,低位丢弃) | 60(0011 1100) >> 2 = 15(0000 1111) |