目录
带头单链表
一、概念
带头单链表中默认带有一个虚拟头结点dummyHead
,不存储有效元素。
二、要点总结
-
链表中仅创建了虚拟头结点的Node对象
private Node dummyHead = new Node();
虚拟头结点的属性值val默认为0(不存储有效元素),next引用默认为null
-
带有虚拟头结点后,头结点也具有了前驱结点,带头单链表中所有的结点都有了前驱结点
所有结点的插入、删除方式均相同,无需再对头结点进行单独处理,无需单独考虑头结点
-
插入、删除操作的核心即寻找前驱结点,带头单链表中第一个前驱结点即为虚拟头结点
-
遍历单链表中的每个元素
-
遍历带头单链表的每个元素:从第一个有效元素开始(越过虚拟头结点)
Node x = dummyHead.next;
-
遍历不带头单链表的每个元素:从头结点开始
Node x = head;
-
-
在带头单链表中根据index寻找结点
从虚拟头结点向后走
Node x = dummyHead;
-
寻找index的前驱结点:从虚拟头结点开始向后走 index 步即到达index的前驱结点
for(int i = 0; i < index; i++)
—— 循环执行index次 -
寻找index结点:从虚拟头结点开始向后走 (index+1) 步即到达index结点
for(int i = 0; i < index+1; i++)
—— 循环执行(index+1)次
-
三、代码实现
1. 类结构
//结点类
class Node {
int val; //当前结点保存的值
Node next; //下一个结点的地址
public Node(int val) {
this.val = val;
}
public Node(int val, Node next) {
this.val = val;
this.next = next;
}
public Node() {};
}
//带头单链表
public class SingleLinkedListWithDummyHead {
private int size; //链表中有效元素的个数(不包含虚拟头结点),第一个有效元素的索引为0
private Node dummyHead = new Node(); //虚拟头结点
...(增删改查等方法)
}
2. 带头单链表的遍历
【遍历单链表中的每个元素】
遍历带头单链表的每个元素:从第一个有效元素开始(越过虚拟头结点)
Node x = dummyHead.next;
√遍历不带头单链表的每个元素:从头结点开始
Node x = head;
/** 遍历带头单链表 */
public String toString() {
String ret = "";
Node x = dummyHead.next; //从第一个有效元素开始遍历
while (x != null) {
ret += x.val + "->";
x = x.next;
}
ret += "NULL";
return ret;
}
3. 插入结点
3. 1. 头插法
【不带头、带头单链表的头插法区别】
不带头单链表:对于原头结点存在(链表非空)、原头结点不存在(链表为空)的两种情况,头插操作部分不同
带头单链表:无论链表是否为空,原头结点均可视为存在,只是空链表的原头结点为空结点,而非空链表的原头结点为有效结点,两种情况的头插操作完全相同
/**(头插)无论原头结点是否存在(链表是否为空),头插操作都一样
* 若链表不为空,则虚拟头结点next指针指向的原头结点为有效结点head
* 若链表为空,则虚拟头结点next指针指向的原头结点为空结点(null)
*/
public void addFirst(int val) {
/*
1.新建结点
Node newHead = new Node();
newHead.val = val;
2.新结点的next指针指向原头结点(若链表为空,则原头结点为空)
newHead.next = dummyHead.next;
3.虚拟头结点的next指针指向新结点
dummyHead.next = newHead;
*/
dummyHead.next = new Node(val, dummyHead.next);
//有效元素增1
size++;
}
3.2. 指定索引处插入结点
在index处插入结点需找到index的前驱结点。
对于带头单链表,所有的结点包括头结点都有前驱结点,故所有结点的插入操作都相同,无需单独考虑头结点。
【在带头单链表中根据index寻找结点】
从虚拟头结点向后走
Node x = dummyHead;
寻找index的前驱结点:从虚拟头结点开始向后走 index 步即到达index的前驱结点
for(int i = 0; i < index; i++)
—— 循环执行index次寻找index结点:从虚拟头结点开始向后走 (index+1) 步即到达index结点
for(int i = 0; i < index+1; i++)
—— 循环执行(index+1)次
/**(指定index处插入元素)前驱结点指针prev从虚拟头结点开始向后走,需要走index步即找到插入位置的前驱结点 ==> 循环index次
* 链表中包含了虚拟头结点后,头结点也有了前驱结点,即所有的结点都有了前驱结点,插入、删除的操作均一致,无需再对头结点进行单独处理
*/
public void add(int index, int val) {
//【传参涉及index就要判断其合法性】允许(index=0)头插、(index=size)尾插
if(index < 0 || index > size) {
System.err.println("add index illegal!");
return;
}
//从虚拟头结点开始向后寻找index的前驱结点
Node prev = dummyHead;
//走index步即找到prev结点 ==> 循环index次
for(int i = 0; i < index; i++) {
prev = prev.next; //prev向后移动一步
}
/*
1.新建结点
Node node = new Node();
node.val = val;
2.新结点的next指针指向index结点
node.next = prev.next;
3.前驱结点的next指针指向新结点
prev.next = node;
*/
prev.next = new Node(val, prev.next);
//有效元素增1
size++;
}
3.3 尾插法
可直接复用指定索引插入结点的方法add(int index, int val)。尾结点有前驱结点,同其他所有结点的插入操作相同。
/**(尾插法)尾结点同其他结点一样具有前驱结点,可直接复用add(int index, int val)方法 */
public void addLast(int val) {
add(size, val);
}
4. 删除结点
4.1. 删除指定索引处的结点
删除index处的结点需先找到index的前驱结点。
对于带头单链表,所有的结点包括头结点都有前驱结点,故所有结点的删除操作都相同,无需单独考虑头结点。
/** 判断index索引的合法性 —— 查询、删除、修改,设定为private私有(用户不关心)*/
private boolean rangeCheck(int index) {
if(index < 0 || index >= size) { //必须在有效元素的范围内,不允许(index=size)
System.err.println("index illegal!");
return false;
}
return true;
}
/** 删除指定索引处的结点,返回被删除的结点值,索引越界返回-1 */
public int remove(int index) {
//【传参涉及index就要判断其合法性】
if(!rangeCheck(index)) {
return -1;
}
//从虚拟头结点开始向后寻找index的前驱结点
Node prev = dummyHead;
//向后走index步即找到前驱结点
for(int i = 0; i < index; i++) {
prev = prev.next;
}
//找到index的前驱结点,暂存index结点值用于返回
Node node = prev.next;
int temp = node.val;
//删除index结点
prev.next = node.next; //前驱结点的next指针指向后驱结点
node.next = node = null; //指向后驱结点的next指针置空,将node自身也置空
size--;
return temp;
}
4.2 删除所有值为val的结点
1)考虑链表中出现多个连续相邻的val要删除的情况:
设置循环,每次都只删除一个结点,删除后即检查此结点的后继结点是否还是val,若还是val则继续删除此结点,若不是val则向后移动前驱结点指针prev,寻找下一个要删除的val
2)由于带头单链表的所有结点(包括头结点)都有前驱结点,故无需单独考虑头结点
/** 删除所有值为val的结点 */
public void removeByValueAll(int val) {
//寻找val结点的前驱结点
Node prev = dummyHead; //虚拟头结点是链表中的第一个前驱结点
while (prev.next != null) { //后方还有结点
if (prev.next.val == val) { //循环删除所有值为val的结点
//删除结点
Node node = prev.next;
prev.next = node.next;
node.next = node = null;
size--;
} else { //prev.next不是待删除结点
prev = prev.next; //移动prev指针
}
}
}