1. 前言:
先来复习下链表的概念:
物理结构
计算机的物理存储结构有两种:{顺序,链式}
物理存储结构 | 定义 | 存取速率 | 插入删除速率(给定地址) | 插入删除速率(给定值) | 空间占用 | 空间利用率 |
---|---|---|---|---|---|---|
链式 | 数据存储在地址随机的物理存储单元 | 低 | 高 | 中 | 高 | 高 |
顺序 | 数据存储在地址连续的物理存储单元 | 高 | 中 | 低 | 低 | 低 |
逻辑结构
链表的最大优势就是给定数据地址时,插入删除的速度为O(1), 最大缺点是随机读取和存储速率感人。指针也增加了额外的内外存空间。双向链表对于单向链表添加了pre前驱指针,如果是链表的值是顺序的,大量的随机访问平均只需遍历表的一半。
2. 正题:
手动实现一个双向链表,实现CRUD基本功能
API按照JDK-11里的LinkedList:
https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/util/LinkedList.html
类图:
代码:
MyLinkedList的内部成员:
public class MyLinkedList<E>{
private class Node<E>{
E item;
Node<E> pre;
Node<E> next;
public Node(E item, Node<E> pre, Node<E> next) {
this.item = item;
this.pre = pre;
this.next = next;
}
private int size;
private Node<E> head;
private Node<E> tail;
}
MyLinkedList构造方法:
public MyLinkedList(){}
/*
* construct a new MyLinkedList that contains an element of E
* @param element
* the first element as well as the head node and tail node of the list
*/
public MyLinkedList(E element){
this.size = 1;
this.head = new Node<E>(element,null,null);
this.tail = head;
}
add(E element):boolean接口实现:
/*
* append an element of E to the last of the list
* @param element
* the element to be added to the list
*/
public boolean add(E element){
if(size==0){
this.size = 1;
this.head = new Node<E>(element,null,null);
this.tail = head;
return true;
}else{
//generate a new node with pre -> the current tail of the list and next -> null
Node<E> node = new Node<E>(element, tail,null);
//the next of current tail -> the new node
this.tail.next=node;
//the list set tail as the new node
this.tail = node;
this.size++;
return true;
}
}
PS:这里是尾部插入哦!
重写toString:
@Override
public String toString() {
StringBuffer str = new StringBuffer();
if(this.size>0){
//append all available nodes to str,starting from the head
Node<E> iterator = this.head;
str.append("[ ");
str.append(iterator.item+" ");
//append nodes to str until tail being found
while(iterator.next!=null){
iterator = iterator.next;
str.append(iterator.item+" ");
}
str.append("]");
return str.toString();
}else{
return "[]";
}
}
说明: 这里用StringBuffer将所有Node中的数值一个个存入字符串缓冲区,以便日后显示我们的链表元素
先来测试下add功能
public static void main(String[] args) {
var list1 = new MyLinkedList<Integer>();
System.out.println(list1);
list1.add(1);
list1.add(2);
System.out.println(list1);
var list2 = new MyLinkedList<Integer>(1);
list2.add(2);
list2.add(3);
list2.add(4);
System.out.println(list2);
}
输出:
[]
[ 1 2 ]
[ 1 2 3 4 ]
嗯,add成功(好吧其实途中改了几个bug才成功的…)
add(int index, E element):boolean接口实现
这里双链表的优势来了,给定了位置,那么可以判断从头还是从尾部找,这里分了2步,一步定位,一步插入,
/*
* Inserts the specified element at the specified position in this list.
* Shifts the element currently at that position (if any)
* and any subsequent elements to the right (adds one to their indices).
* @index
* the position of the list to be inserted
* @element
* the element node of E to be inserted
*/
public boolean add(int index, E element){
//add to the last of the list?
if((this.size==0 && index==0)||index==this.size){
add(element);
return true;
}
//index out of boundary
if(index > this.size || index < 0){
return false;
}
//index within the boundary
else{
if(index+1 > this.size/2){
//starting from the tail
int count = this.size-1;
Node<E> iterator = this.tail;
while(index != count){
iterator = iterator.pre;
count--;
}
//insert before this node
insertNode(iterator,element);
return true;
}else{
//staring from the head
int count = 0;
Node<E> iterator = this.head;
while(index != count) {
iterator = iterator.next;
count++;
}
//insert before this node
insertNode(iterator,element);
return true;
}
}
}
/*
* insert an element before the node of which the iterator referenced
* @iterator
* a reference to the node to be unlinked from its prev node
* @element
* a new node of which prev -> iterator.prev & next -> iterator
*/
private void insertNode(Node iterator, E element){
Node<E> node = new Node<E>(element,iterator.pre,iterator);
// is the new node the head?
if(iterator == this.head){
this.head = node;
}
if(iterator.pre != null){
iterator.pre.next = node;
}
iterator.pre = node;
this.size++;
}
测试:
public static void main(String[] args) {
var list3 = new MyLinkedList<Integer>();
//out of boundary
list3.add(1,0);
System.out.println(list3);
//add to the last
list3.add(0,0);
System.out.println(list3);
for(int i=1;i<10;i++){
list3.add(i);
}
System.out.println(list3);
list3.add(10,10);
System.out.println(list3);
//index is before the first half elements
list3.add(5,11);
System.out.println(list3);
//index is not before the first half elements
list3.add(6,12);
System.out.println(list3);
//index is before the first half and is the head
list3.add(0,255);
System.out.println(list3);
}
输出
[]
[ 0 ]
[ 0 1 2 3 4 5 6 7 8 9 ]
[ 0 1 2 3 4 5 6 7 8 9 10 ]
[ 0 1 2 3 4 11 5 6 7 8 9 10 ]
[ 0 1 2 3 4 11 12 5 6 7 8 9 10 ]
[ 255 0 1 2 3 4 11 12 5 6 7 8 9 10 ]
嗯,看起来add实现成功了。
插入的api代码调试中遇到的bug:
主要是移动的几种情况没考虑清楚,插入头节点,只移动右节点;插入非头非尾,移动双边节点;插入尾节点,移动左节点。
- 没有考虑插入时,如果是头节点的情况(头节点前没有节点,因而遇到runtime NullPointerException)
- 没有考虑插入时,是尾节点的情况,就应该直接add。
remove(E element):boolean接口实现
删除一个节点,如果删除头尾,只移动一边;如果删除非头非尾节点,移动两边节点
这里按照LinkedList中接口定义,从头向下找到第一个element删除
这边手动写发现unlink和relink过程可能需要重构,先写了后看LinkedList源码再改。
/*
* Removes the first occurrence of the specified element from this list, if it is present.
* If this list does not contain the element, it is unchanged.
* @param element
* the element to be removed,if present
*/
public boolean remove(E element){
Node<E> iterator = this.head;
if(iterator == null){
return false;
}else{
//if the head contains the target element
if( iterator.item.equals(element) ){
//if the head has a node linked to it
if(iterator.next!=null){
//unlink reference to the head,set next node as the head
iterator.next.pre=null;
this.head = iterator.next;
//else the head is the only node in the list
}else{
//just set the head as null
this.head = null;
}
//remove all links of the original head
iterator.next = null;
iterator.item = null;
this.size--;
return true;
//head is not the target, search from its next, if presents
}else{
while(iterator.next != null){
iterator = iterator.next;
if( iterator.item.equals(element) ){
//found the target, if it has a node next to it
if(iterator.next!=null){
//re-link left and right nodes
iterator.next.pre = iterator.pre;
iterator.pre.next = iterator.next;
//remove target links
iterator.next = null;
iterator.pre = null;
//then the target is the tail
}else{
iterator.pre.next=null;
this.tail = iterator.pre;
iterator.pre = null;
}
iterator.item = null;
this.size--;
return true;
}
}
}
}
return false;
}
测试:
public static void main(String[] args) {
var list4 = new MyLinkedList(1);
for(int i=2;i<10;i++){
list4.add(i);
}
System.out.println("Original list: " + list4);
list4.remove(1);
System.out.println("head deleted: " + list4);
list4.remove(9);
System.out.println("tail deleted: " + list4);
list4.remove(5);
System.out.println("target deleted in the middle: " + list4);
list4.remove(5);
System.out.println("non-existing target: "+ list4);
}
结果:
Original list: [ 1 2 3 4 5 6 7 8 9 ]
head deleted: [ 2 3 4 5 6 7 8 9 ]
tail deleted: [ 2 3 4 5 6 7 8 ]
target deleted in the middle: [ 2 3 4 6 7 8 ]
non-existing target: [ 2 3 4 6 7 8 ]
看起来测试通过了,证明链表的节点值和指向都没错,但这里拆掉的节点是否被垃圾回收无法验证,可能有内存泄露。所以要检查代码是否清空目标节点的成员,具体运行如何测试内存泄露以后再看。
set(int index, E element):E 接口实现
从头结点(0位)开始,替换第index位的值,返回替换前的值。
要检查set功能,当然先get咯,我们需要先实现get(int index,E element): E 接口
/*
* Returns the element at the specified position in this list.
* @param index
* the position of the target element
*/
public E get(int index){
if(index < 0 || index >= this.size){
return null;
}else{
int count = 0;
Node<E> iterator = this.head;
while(iterator != null){
if(count == index){
return iterator.item;
}
count++;
iterator = iterator.next;
}
}
return null;
}
然后是set, 几乎和get一样…
/*
*Replaces the element at the specified position in this list with the specified element.
* @param index
* the position of which the target element to be replaced
* @param element
* the element to replace the target
*/
public E set(int index, E element){
if(index < 0 || index >= this.size){
return null;
}else{
int count = 0;
Node<E> iterator = this.head;
while(iterator != null){
if(count == index){
E oldElement = iterator.item;
iterator.item = element;
return oldElement;
}
count++;
iterator = iterator.next;
}
}
return null;
}
测试:
public static void main(String[] args) {
var list5 = new MyLinkedList();
for(int i=0;i<11;i++){
list5.add(i);
}
System.out.println(list5);
list5.set(0,-1);
System.out.println(list5.get(0));
System.out.println(list5);
}
结果
[ 0 1 2 3 4 5 6 7 8 9 10 ]
-1
[ -1 1 2 3 4 5 6 7 8 9 10 ]
好了,这里MyLinkedList的equals和hashcode没重写,不过大概手动完成了双向链表的增插删改,接下来就直接看LinkedList源码重构吧。