0.算法与数据结构编程时应遵守的一般性规范
- step1.定义清楚算法数据结构各种状态含义、前置条件、后置条件和副作用
数组索引、引用对象(链表结点、树结点)不能为空 - step2.用注释+部分代码的方式勾勒出代码骨架,清晰表达出前置条件、后置条件、副作用(各种状态变量的变化);方法的主要步骤(对复杂方法进行模块化拆分)
- step3.对于复杂数据结构,例如链式结构(链表、树等),可以借助画图,并检查各修改结点的各状态是否正确被赋值
- step4.特别注意特殊情况、边界情况的处理:溢出、一个元素或者无元素等等
- step5.正确写好循环,三要素(初始化、循环条件、更新)以及循环不变量的维护
- step6.正确写好递归:起始条件(终止条件)、递归前后状态需要保持不变
1.定义清楚方法的前置条件、后置条件和副作用
1)前置条件——在方法执行前必须为真。除非前置条件满足,否则不应该使用方法,也不能期待方法能正确执行。(表示函数对其参数的期望和/或函数可能使用的对象的状态。)
- 数组——删除时不能为空
- 栈——pop()时不能为空
- 队列——出队时队列不能为空
- 哈希表——removeNode删除时不能为空
- 凡是涉及数组的相关算法、数据结构——都需要对索引范围的检查
- 引用类型(二叉树结点、红黑树结点等)——获取其域时都必须进行非空判断
- 数组等——使用前必须初始化
2)后置条件——个后置条件是,应当在举行谓词从退出的功能。它表达了条件,一个函数应确保为返回值和/或可以由函数中使用的对象的状态。
- 数组类型、集合类型—删除时需要将对应的位置置为null
- 引用类型、链表结构的结点类型—删除时将其域引用的对象置为null
- 数组、集合——add remove时,需要相应地更新其状态,比如size、count
- 栈——在扩容时,也需要正确地更新head、tail状态
- 链式哈希表——扩容时,将旧表数组移动到新表中时,需要将旧表对应位置置为null
- 开放式哈希表——删除,需要将该位置之后的数据重新插入
- BFS最短路径——不要忘记更新marked[w]
- 图——添加边时不要忘了E++
2.清晰定义状态变量,并维护状态变量含义不变
- 栈
head——指向栈顶元素
tail—— 指向栈底元素的下一个,(不考虑循环数组)因此栈中元素范围[head, tail) - 堆
清晰定义——是从索引为1开始的,[1, size]存有元素 - 索引优先队列
在堆数组中存储索引i,同时通过数组keys将索引i与key关联起来,保证堆性质的比较还是通过key来实现的(只不过需要通过i来获取对应的key,多了这一道取值的过程)
3.for循环、while循环的处理
循环语句三要素:
- 控制变量的初始化,不要想当然的初始化为0
partition(int left, int right),循环变量初始化为left - 循环条件
- 循环控制变量的更新
1)要特别定义清楚循环各变量含义
2)定义清楚循环不变量含义,并保证循环不变量在循环中始终成立;特别注意在循环内部处理数组索引相关时,不要忘了范围检查
3)控制变量的正确更新,不要漏掉相关变量的更新
-
链式哈希表
对于while循环,要清楚各个变量的定义以及三个要素,并且不要忘了更新size。
特别重要的是p与node的含义:
p——是(hash,key)的前驱结点,两种情况:在链表末尾和在链表中间
node——是(hash, key)结点本身,两种情况:在链表末尾(此时node为null)和在链表中间 -
堆siftUp siftDown与插入排序
while循环内是queue[parent],循环外也是;
在循环内涉及到索引right和left,也要进行索引是否超出范围的检查。 -
将rnd >>>=1错写成rnd>>>1,导致死循环
-
变量更新不要随意写入for()最后一部分,可能会触发对空指针异常
for (Index<K,V> prev = head; prev != null; prev = prev.down) {
// 这一句不能放在for循环更新语句中,只能放在这里,因为prev可能为null,所以只有检查完之后才能赋值
Index<K,V> right = prev.right;
4.复杂方法和数据结构的处理方式
- 对于链式数据结构,先画图,降低处理的复杂度
链表
红黑树的leftRotate()、rightRotate()少了更新xy这一对关系 - 对于复杂的操作
先理清思路,写出完整操作步骤,再写代码,然后再进行优化合并。 - 红黑树null结点的处理
《算法导论》null结点有作用的!这个处理参考parentOf()、leftOf()、rightOf()、colorOf()
5.正确编写递归方法
参考数学归纳法:
- 1)命题对自然数N = 1(或N=0)成立
- 2)假设命题对N - 1成立,能够推导出命题也对N成立
- 3)则命题对所有自然数都成立
两个核心要素:
- 1)起始条件(终止条件)
- 2)递推关系
递归调用,注意状态的维护
private void dfs(int v) {
marked[v] = true;
onStack[v] = true;
for (int w : G.adj(v)) {
if (hasCycle()) {
return;
}
if (!marked[w]) {
edgeTo[w] = v;
dfs(w);
} else if (onStack[w]) {
cycle = new ArrayDeque<>();
cycle.push(w);
for (int u = v; u != w; u = edgeTo[u]) {
cycle.push(u);
}
cycle.push(w);
}
}
// 特别注意,不要忘了这个,递归代码必须遵守的规则
onStack[v] = false;
}
6.特殊情况、边界处理
- 数组、集合等——扩容时,需要正确处理溢出情况、扩容大小比最小要求还小等各种情况
- 链表类——处理特殊情况(链表为空、一个元素)
- keysWithPrefix()方法,prefix本身忘记处理
7.其他细节知识点
- 正无穷
Arrays.fill(distTo, Double.POSITIVE_INFINITY);
- 空数组
private static final Object[] EMPTY_ELEMENTDATA = {};
网友评论