自己动手实现java数据结构(二) 链表(2)
链表内部维护着首、尾哨兵两个内部节点(双端),作为链表的第一个和最后一个节点,新插入的节点总是处于这两个哨兵节点之间,用户也无法感知哨兵节点的存在。哨兵节点并不用于保存数据,其存在的主要目的是为了简化边界条件的逻辑。 举个例子:在节点插入时,必须判断当前是否第一个节点,来决定是否需要建立和之前节点的联系;在节点删除时,也必须判断自己的左右节点是否存在,避免空指针异常。首尾哨兵节点的引入使得任何情况下,节点插入,删除时都可以确保其左右节点一定存在,从而避免大量的异常判断。 可以看到,哨兵节点的引入简化了代码在边界条件时的各种判断逻辑。这提高了执行效率,更重要的是降低了复杂性,使代码变得更容易理解,也更可靠。 { * 链表 头部哨兵节点 * private Node<E> first; * 链表 尾部哨兵节点 * last; * 链表 逻辑大小 * size; public LinkedList() { this.first = new Node<>(); this.last = (); :::将首尾哨兵节点 进行连接 this.first.right = .last; this.last.left = .first; :::初始化size this.size = 0; } } 3.3?较为简单的?size(),isEmpty(),indexOf(),contains()方法实现:@Override size() { return .size; } @Override isEmpty() { return (this.size == 0); } @Override indexOf(E e) { :::当前节点 = 列表头部哨兵 Node<E> currentNode = if(e != null){ :::如果不是查询null元素 :::遍历列表 for(int i=0; i<this.size; i++){ :::不断切换当前节点 ==> "当前节点 = 当前节点的右节点" currentNode = currentNode.right; :::如果满足要求(注意: equals顺序不能反,否则可能会有currentNode.data为空,出现空指针异常) if(e.equals(currentNode.data)){ :::返回下标 return i; } } }else{ :::如果是查询null元素 :::如果是null元素 if(currentNode.data == ){ i; } } } :::遍历列表未找到相等的元素,返回特殊值"-1"代表未找到 return -1; } @Override contains(E e) { :::复用indexOf方法,如果返回-1代表不存在;反之,则代表存在 return (indexOf(e) != -1); } 链表 indexOf、contains方法的时间复杂度:indexOf方法的实现和向量类似,是通过一次循环遍历来进行查询的,从头部节点不停地跳转到下一个节点,直到发现目标节点或者到达尾部哨兵节点。 因此indexOf方法、contains方法的时间复杂度都是O(n)。 3.4 链表增删改查接口实现3.4.1?下标越界检查链表基于下标的增删改查操作都需要进行下标的越界检查。这里优化了前面"向量篇"博客中提到的缺陷,针对不同的错误,会抛出不同类型的自定义异常,使客户端可以进行更细致的异常处理。 * 插入时,下标越界检查 * index 下标值 void rangeCheckForAdd( index){ :::如果大于size的值,抛出异常 :::注意:插入时,允许插入线性表的末尾,因此(index == size)是合法的 if(index > .size){ throw new OutOfIndexBoundException("index error index=" + index + " size=" + .size) ; } if(index < 0new NegativeOfIndexException("index error index=" + index + " size=" + .size) ; } } * 下标越界检查 * void rangeCheck(:::如果大于等于size的值,抛出异常 if(index >= .size) ; } } 同时,链表基于下标的操作都需要先查询出下标对应的内部节点,通过操纵对应的内部节点来进行相关操作。因此需要抽象出一个通用的查询方法。 * 返回下标对应的 内部节点 * private Node<E> find(:::要求调用该方法前,已经进行了下标越界校验 :::出于效率的考虑,不进行重复校验 :::判断下标在当前列表的大概位置(前半段 or 后半段)尽量减少遍历查询的次数 if(index < this.size/2:::下标位于前半段 :::从头部结点出发,进行遍历(从左到右) Node<E> currentNode = .first.right; :::迭代(index)次 int i=0; i<index; i++){ currentNode = currentNode.right; } currentNode; }:::下标位于后半段 :::从尾部结点出发,进行遍历(从右到左) Node<E> currentNode = .last.left; :::迭代(size-index-1)次 int i=index; i<size-1; i++ currentNode.left; } currentNode; } } find方法的时间复杂度?:可以看到,find方法会根据下标所处的大概位置决定开始遍历的方向(尽量减少迭代查询的次数),下标越接近头部或者尾部,查询次数越少。因此下标在靠近头部、尾部时效率较高,而在居中位置效率较低。 (编辑:ASP站长网) |