Java算法与数据结构规范

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 prev = head; prev != null; prev = prev.down) {
                // 这一句不能放在for循环更新语句中,只能放在这里,因为prev可能为null,所以只有检查完之后才能赋值
                Index  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 = {};

你可能感兴趣的:(Java算法与数据结构规范)