Linked List - tenji/ks GitHub Wiki
链表(Linked List)
链表是一种物理存储单元上非连续、非顺序的存储结构,链表中的元素使用指针链接。
链表由 N 个节点组成的,每一个节点就是一个对象,每一个节点分为两部分:
- 数据域(用于存储数据)
- 指针域(用于连接各个链表)
注意:头结点不存储数据,其数据域一般无意义(当然有些情况下也可存放链表的长度、用做监视哨等等)。
为何引入头结点?
-
对链表的删除、插入操作时,第一个结点的操作更方便;
如果链表没有头结点,那么头指针指向的是链表的第一个结点,当在第一个结点前插入一个节点时,那么头指针要相应指向新插入的结点,把第一个结点删除时,头指针的指向也要更新。也就是说如果没有头结点,我们需要维护着头指针的指向更新,因为头指针指向的是链表的第一个结点。如果引入头结点的话,那么头结点的 next 始终都是链表的第一个结点。
-
统一空表和非空表的处理
有了头结点之后头指针指向头结点,不论链表是否为空,头指针总是非空,而且头结点的设置使得对链表的第一个位置上的操作与在表中其它位置上的操作一致,即统一空表和非空表的处理。
头指针和头结点的区别?
链表中第一个结点的存储位置叫做头指针。头结点是为了操作的统一与方便而设立的,放在第一个元素结点之前。链表中可以没有头结点,但不能没有头指针。如果链表有头结点,那么头指针是指向头结点的指针,如果没有头结点,那么头指针就是指向第一个元素结点的指针。
有头结点的单链表结构:
无头结点的单链表结构:
可以分为以下几种类型的链表:
一、单链表(Singly Linked List)
1.1 节点结构
public class Node <E> {
private Node<E> next; // 指向下一个节点
private E e; // 数据域
}
附加信息:头节点、尾节点、链表长度等
// 声明头节点
private Node<E> head;
// 声明尾节点
private Node<E> tail;
// 链表的大小
private int size;
/**
* 获取单链表中存储的元素总数
*
* @return 返回 size 属性
*/
public int size() {
return size;
}
1.2 增加节点
public void add(E e) {
// 以 e 实例化一个节点
Node<E> node = new Node<E>(e);
// 往尾节点后加节点
tail.setNext(node);
// 该节点设为最后一个节点
tail = node;
size++;
}
1.3 删除节点
/**
* 删除指定的节点 e,并返回 e
*/
public E delete(int index) {
if (index < 0 || index > size)
return null;
if (index == 0) { // 当索引为 1 时,令头节点的下一个节点为头节点
Node<E> node2 = head.getNext();
head.setNext(node2.getNext());
size--;
return node2.getE();
}
// 获取要删除节点的前一个节点
Node<E> bNode = select(index - 1);
// 获取要删除的节点
Node<E> node = bNode.getNext();
// 获取要删除节点的后一个节点
Node<E> nNode = Node.getNext();
// 先建立删除节点的前一个节点和删除节点的后一个节点的关系
bNode.setNext(nNode);
// 清除 node 的下一个节点
node.setNext(null);
size--; // 计数器减 1
return node.getE();// 返回删除节点中的数据域
}
1.4 修改节点
/**
* 修改指点位置的数据域
*/
public E update(E x, int index) {
if (index < 0 || index > size || size == 0)
return null;
Node<E> xnode = new Node<E>(x); // 获取一个新节点
Node<E> node = select(index);
node.setE(xnode.getE());
return node.getE();
}
1.5 查询节点
/**
* 获取指定索引位置的节点对象
*/
private Node<E> select(int index) {
// 将头节点的下一个节点赋给 node
Node<E> node = head.getNext();
for (int i = 0; i < index; i++) {
node = node.getNext(); // 获取 node 的下一个节点
}
return node;
}
/**
* 找到指定节点的数据域,并返回
*/
public E getE(int index) {
if (index < 0 || index > size - 1)
return null;
Node<E> node = select(index); // 查找指定索引位置的节点对象
return node.getE(); // 获取节点中的数据域元素并返回
}
以上部分组成了简单的单链表,有了以上部分就可以完成手写单链表的任务了。
二、双链表(Doubly Linked List)
- 与单链表对比,双链表需要多一个指针用于指向前驱节点,因此如果存储同样多的数据,双向链表要比单链表占用更多的内存空间;
- 双链表的插入和删除需要同时维护 next 和 prev 两个指针;
- 双链表中的元素访问需要通过顺序访问,支持双向遍历,这就是双向链表操作的灵活性根本。
2.1 节点结构
public class Node <E> {
private Node<E> pre; // 指向上一个节点
private Node<E> next; // 指向下一个节点
private E e; // 数据域
}
添加元素
删除元素
查询元素
三、循环链表(Circular Linked List)
...
四、双循环链表(Doubly Circular linked list)
4.1 节点结构
public class Node <E> {
private Node<E> pre; // 指向上一个节点
private Node<E> next; // 指向下一个节点
private E e; // 数据域
}
附加信息:头节点、链表长度等
// 声明头节点
private Node<E> head;
// 链表的大小
private int size;
/**
* 获取单链表中存储的元素总数
*
* @return 返回 size 属性
*/
public int size() {
return size;
}
链表初始化:
public DoublyLinkedList() {
/**
* 头结点不存储值,并且头结点初始化时,就一个头结点。(伪头结点)
*/
head = new Node<>(null, null, null);
head.pre = head.next;
head = head.next;
size = 0;
}
4.2 增加节点
/**
* 添加元素(尾部插入)
*
* @param e 节点值
*/
public void add(E e) {
Node<E> cur = new Node<>(head.pre, head, e);
/*
1、新节点的前一个节点的后一个节点为新节点;
2、新节点的后一个节点的前一个节点是新节点;
*/
head.pre.next = cur;
head.next = cur;
size++;
return;
}
/**
* 添加元素(头部插入)
*
* @param e 节点值
*/
public void addToHead(E e) {
Node<E> cur = new Node<>(head, head.next, e);
head.next.pre = cur;
head.next = cur;
size++;
return;
}
4.3 删除节点
/**
* 根据索引删除元素
*
* @param index 待删除节点对应的索引
*/
public void delete(int index) {
Node<E> cur = get(index);
cur.pre.next = cur.next;
cur.next.pre = cur.pre;
size--;
// 释放当前节点
cur = null;
return;
}
public void deleteHead() {
delete(0);
}
public void deleteTail() {
delete(size - 1);
}