持续更新
位运算
用移位代替乘除法
- 移位的效率远高于乘除法
- 除以2时一般直接右移一位即可
- 乘以2时一般直接左移一位即可
用位运算判断奇偶性
- 将整数与1做与运算,值为1表示为奇数,为0表示为偶数
判断二进制数中1的个数
方法一: n=(n-1)&n
n = (n-1) & n
可以将二进制数n中的最后一位1变成0- 不停执行该语句,知道n为0,执行次数即二进制数n中1的数量
方法二: result=n&flag
- flag初始化为与n相同的类型(二进制位数一致)
- flag从1,2,4….,为2的次方数,即初始化为1,每次向左移动一位,直到flag为0为止(此时说明flag移位溢出了,n的每一位都进行过比较了)
- 累计result为1的次数就是二进制数n中1的数量
错误方法: result=n&1
- 这种方法不设置flag变量,每次将n左移一位,直到n为0
- 当n为负数时会引发无线循环,所以此方法是错的
小数的精度
比较两个小数是否相等
- 无论a,b类型是否相等(float, double),都不能直接使用
==
符号直接比较 - 定义以下函数实现比较即可|a-b| < \(\epsilon\) (\(\epsilon\)是一个误差允许的极小值)
1
2
3
4
5bool Equal(double num1, double, num2){
if(num1-num2 < 0.0000001 && num1-num2 > -0.0000001)
return true;
return flase;
}
为什么存在精度问题
因为十进制的小数存到内存时是二进制,此时有很多无线循环,但是二进制位数有限,所以有误差
一般来说,
double
类型足以满足我们日常计算的精度,在无需很高精度时使用float
可以减少内存占用实例:
1
2
3
4
5
6
7
8double a = 0.9;
float b = 0.9;
if(a == (double)b)
printf("a==b is true")
else
printf("a==b is flase")
# output:
a==b is false- 对于小数
0.9
,表示为二进制后小数部分是无线循环小数,在float
类型时与double
类型时由于保留的小数位不同,所以值不同
- 对于小数
最大最小整数
最小负数
1 | # int |
最大正数
1 | # int |
第一要务:输入检查
- 无论何时,函数实现的第一步都应该是检查输入是否合法
常见的检查
- 指针:是否为空
- 整数:是否满足条件(大于0,小于0等)
- 多个整数之间的关系:是否满足大小关系等
访问数组时必须首先确认
- 数组已初始化
- 访问的Index没有超过数组
关于折半搜索和查找
- 一般来说low从0开始,high从数组长度开始(注意,有时候high如果从len-1开始则可能造成无法访问到最后一个位置?)
- 相关题目:LeetCode 35题 Search Insert Position
mid定义为(low + high)/2
如果
mid
的定义如下1
mid = (low + high) / 2
那么每次查找结束时更新时应该作如下更新
1
2low = mid + 1
high = mid或者
1
2low = mid + 1
high = mid - 1如果此时使用下面的语句作为更新
1
low = mid
low == high - 1
有low == mid
,上面的式子将造成死循环
mid定义为(low + high + 1)/2
如果
mid
的定义如下1
mid = (low + high + 1) / 2
那么每次查找结束时更新时应该作如下更新
1
2low = mid
high = mid - 1或者
1
2low = mid + 1
high = mid - 1如果使用下面的语句作为更新
1
high = mid
low == high - 1
有high == mid
,上面的式子将造成死循环
二分查找相等的数时往往可以使用
1
2low = mid + 1
high = mid - 1但是查找的是当前数组不存在的节点的存放位置时要注意查找时
1
2low = mid + 1
high = mid- 这样确保i和j退出时相等
- 相关题目:LeetCode 35 Search Insert Position
相关题目
- 详细情况参考LeetCode 34题 Find First and Last Position of Element in Sorted Array
深度优先遍历和回溯法的异同
深度优先方法(DFS, Depth First Search)使用栈实现,广度优先方法(BFS, Breadth First Search)使用队列实现
一般来说在树中遍历时才有深度优先搜索这种说法,不然比如数组的全部组合遍历都叫做回溯法,而不是DFS
对于树而言,回溯法也是遵循深度优先遍历原则实现的
回溯法可以访问已经访问过的节点,而深度优先一般来说不会再访问已经访问过的节点
DFS是把所有可能的情况考虑到就行了,但回溯法可能会重视访问节点的顺序[LeetCode 79 Word Search]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21def solution(nums):
path, result = [], []
* 如果nums含有重复元素,需要重新排序
def backtrace(index):
if 满足存储条件:
result.append(path[:])
if 不需要继续迭代:
return
* 迭代访问内部节点(不同情况访问方式不同,详情看下面)
* 一种简单的可能是
for i in range(*视具体情况变化):
* 如果nums含有重复元素,需要判断是否跳过(如果nums当前节点与上一个节点重复,则跳过)
path.append(nums[i])
* 不同情况不同回溯方式
* 当path里面不能重复访问同一元素时
* backtrace(i+1)
* 当path里面能重复访问同一元素时
* backtrace(i)
path.pop()
backtrace(0)
return result特殊情况,如果path不能访问同一元素而且不同顺序相同元素算是不同路径的时候,我们可能必须使用rest对象在迭代中记录当前未被访问的对象
- 此时由于不同顺序相同元素算是不同路径,所以我们不能用每次只能向前访问的方法来实现去除重复(及[index:], 这种每次只能向前访问的方法会将先[a, b], [b, a]视为重复而去除,只保留[a, b]当(a < b时))
关于递归函数内部的节点访问方式
其实在使用回溯法访问一个数组时,可以考虑这个我们在访问一棵树,从空节点开始,然后每次迭代所有符合的子节点
| 路径是否可以访问同一节点多次 | 顺序不同节点相同是否算不同路径 | 原始数组中是否包含重复元素 | 预处理 |每一层迭代代码 |
| :——: | :——: | :——: | :——: |
| 是 | 是 | 是 | data.sort() | for i in [:]:
if与前一个相等:
continue
visit(i)
backtrace()
最后移除重复路径|
| 是 | 是 | 否 | - | for i in [:]:
visit(i)
backtrace() [待更新]|
| 是 | 否 | 是 | data.sort() | for i in [index:]:
if与前一个相等:
continue
visit(i)
backtrace(i)
最后移除重复路径|
| 是 | 否 | 否 | - | for i in [index:]:
visit(i)
backtrace(i)|
| 否 | 是 | 是 | data.sort()
rest存储剩余元素| for i in rest:
if与前一个相等:
continue
visit(i)
backtrace(rest-i)|
| 否 | 是 | 否 | rest存储剩余元素 | for i in rest:
visit(i)
backtrace(rest-i) |
| 否 | 否 | 是 | data.sort() | for i in [index:]:
if与前一个相等:
continue
visit(i)
backtrace(i+1) |
| 否 | 否 | 否 | - | for i in [index:]:
visit(i)
backtrace(i+1)|
相关理解
当节点相同顺序不同是相同路径时,使用每次迭代只向后而不向前访问的方式可以避免重复(因为先a后b,与先b后a重复)
- 路径可含有重复节点时,向后迭代从当前节点开始,包括当前节点backtrace(i)
- 路径不可包含重复节点时,向后迭代从当前节点的下一个开始,不包括当前节点backtrace(i+1)
当路径不可包含重复节点时,还可使用rest存储尚未访问的节点或每次迭代时只向后即可解决问题
当数组中包含重复节点时,可以现将其排序,然后在迭代时跳过与前一个节点相同的节点
一个万能的写法是:
- 每次迭代访问所有节点
- 将所有符合条件的路径加入结果
- 最后将重复路径删除即可
1,3这两种情况实际中很少出现,我们不能提前用某种算法确保加入的数据不重复,这时需要我们最终从result中去除重复的(或者每次都查看result中是否有与当前路径相同的路径)
- 第1,3两种情况中,即使我们保证访问顺序完全相同的路径不会重复出现,但是由于数组中可能有重复值,且每个元素可能被访问多次
- 比如[2,1,1]中,容易造成同一个元素a[1]被访问两次的到的[1,1]路径与两个元素a[1],a[2]分别被访问一次得到的[1,1]路径结果完全相同,此时我们只能通过最终手段保证返回结果中路径的唯一性
- 保证路径的唯一性在Python中可以使用将路径转换为
tuple(path)
的方式实现路径的比较,且可以放入set
中 - 第1, 3两种情况的不同之处在于第1种情况中不同顺序的路径是不同的,所以无需对路径进行排序,第3种情况中不同顺序相同结点的路径也是相同结点,则需要先对路径进行排序,再转换为tuple才行
- 第1,3两种情况中,即使我们保证访问顺序完全相同的路径不会重复出现,但是由于数组中可能有重复值,且每个元素可能被访问多次
第4种情况对应LeetCode 39 Combination Sum
第7种情况对应LeetCode 40 Combination Sum II
第8种情况对应LeetCode 216 Combination Sum III
第5种情况对应LeetCode 47 Permutations II
对列表中数字列表的排序
1 | List[List[number]] |
找出所有列表的所有数中最大的数
M
定义一个函数,用一个
M+1
进制的数对每个内层列表进行映射表示系数:
- 需要递增排序的位前面系数为
1
- 需要递减排序的位前面系数为
-1
- 无需排序的位前面系数为
0
- 需要递增排序的位前面系数为
示例:
1
2def key(x):
return x[0]*(M+1)**3 - x[1]*(M+1)**2 - x[2]*(M+1) + x[3]- 对第一和第四位按照递增排序,第二和第三位按照递减排序
映射函数定义后可以使用
sort
函数排序1
l.sort(key=key)
也可以使用
lambda
函数定义1
l.sort(key=lambda x: x[0]*(M+1)**3 - x[1]*(M+1)**2 - x[2]*(M+1) + x[3])
使用 memo 存储中间结果
memo可理解为备忘录,但是和传统的备忘录设计模式有一定区别,后者的目标是恢复对象之前的某个状态,前者的目标更应该理解为动态规划的思想dp存储计算中间值
- 对某些可能会多次被求解的子空间(子数组,子集等), 且子空间能够独立的情况
- 这种题目对应一般可以使用DFS求解,但是由于子空间能够独立,使用DFS会多次求解子空间,从而产生超时的情况
- 子空间能够独立的就应该使用memo
- 如果能够唯一标识(能作为字典的Key值)这些空间数据,即可使用memo存储
- memo 为一个已子空间唯一标识为 Key, 子空间的对应求解结果为Value
- 相关题目: LeetCode 140, Word Break II
在使用特殊字符节省空间
- 在使用DFS搜索空间中的路径时,往往需要记录访问过的点,不能重复访问
- 常用的做法是开辟一个新的相同大小的visited数组,记录是否访问过当前结点
- 一种更好的做法是每次记录当前结点的值, 然后修改为一个特殊字符, DFS 完成后恢复成原始字符, 这种做法无需开辟新的空间