一、说明
链表是一种重要的数据结构,它通过节点间的逻辑链接而非连续内存空间来存储一系列有序的数据元素。每个节点通常包含两个主要部分:数据域(用于存储实际数据)和指针域(用于指向其他节点)。
- 链表分类
1. 单向链表
在单向链表中,每个节点只有一个指针域,即后继指针或next指针,它指向列表中的下一个节点。从头节点开始,通过逐个跟随next指针,可以遍历整个链表。
2. 双向链表
而双向链表是链表的一种扩展形式,每个节点在此基础上具有两个指针域:一个前驱指针或prev指针,用于指向其前一个节点;另一个后继指针或next指针,用于指向其下一个节点。这样,双向链表支持双向的高效遍历,既可以从头节点向后访问,也可以从任意节点向前访问其前驱节点。
3. 循环链表
循环链表是在单链表或双向链表的基础上进行修改得到的一种结构。在循环链表中,最后一个节点的后继指针(对于单链表)或next指针(对于双向链表)不指向NULL或空,而是指向头节点,而双向循环的链表的头节点的前驱指针则指向最后一个节点。从而形成一个闭环状结构。这种数据结构的主要特点是它没有明确的开始或结束,遍历可以从任何一个节点开始,并且在满足条件的情况下可以无限循环下去。循环链表常用于需要周期性访问数据的场景。
总结来说,无论是单链表还是双向链表,其核心思想都是利用节点之间的引用关系组织数据序列,不同之处在于指针域的数量和方向性,以满足不同的操作需求。
二、单向链表
2.1、单向链表的基本概念
单向链表是一种线性数据结构,其每个元素包含两部分:数据域和指针域(或称为引用域)。数据域存储有效信息,指针域则存储指向下一个节点的引用。由于这样的链接方式,信息的存储并不需要连续的内存空间,而是通过“指针”串联起来形成链式存储。
2.2、节点定义
/**
1. 单向链表
*/
public class SinglyLinkedList {
// 头部节点
private Node head;
/**
* 节点定义(内部类)
*/
private static class Node {
// 数据域
int value;
// 指针域,指向下一个节点
Node next;
// 构造函数
public Node(int value, Node next) {
this.value = value;
this.next = next;
}
}
}
2.3、添加节点方法(头插法)
考虑两种情况
- 链表为空:那么head节点就是添加的新节点。新节点的next节点为null;
- 链表不为空:也就说明head节点有值,那么根据头插法。新节点的next节点为head节点;然后将当前添加节点新赋值给head;
/**
* 添加节点(头插法)
*
* @return
*/
public Boolean addFirst(int value) {
//1.链表为空,他的next节点指向为null
if (null == head) {
head = new Node(value, null);
return true;
}
//2.链表不为空。那么当前添加的节点的next指向节点为head;然后将当前添加节点新赋值给head
head = new Node(value, head);
return true;
}
优化版
由于head如果没有值。默认就是null。所以添加方法代码可以简化如下。
/**
* 添加节点(头插法)
*
* @return
*/
public Boolean addFirst(int value) {
//新增节点
head = new Node(value, head);
return true;
}
测试运行添加方法。结果如下。
2.4、添加节点方法(尾插法)
/**
* 添加节点(尾插法)
*
* @return
*/
public Boolean addLast(int value) {
//1.如果链表为空
if (null == head) {
head = new Node(value, null);
} else {
//2.链表不为空
Node last = head;
while (null != last.next) {
last = last.next;
}
last.next = new Node(value, null);
}
return true;
}
2.5、遍历节点方法
/**
* 循环遍历
*/
public void circulate() {
Node node = head;
while (null != node) {
System.out.println(node.value);
node = node.next;
}
}
遍历结果
2.6、获取节点方法
/**
* 根据索引获取值。
*
* @param index
* @return
*/
public Integer get(int index) {
//索引从0开始
int i = 0;
Node node = head;
while (null != node) {
//如果下标值相等。则返回value.否则循环继续。
if (i == index) {
return node.value;
}
node = node.next;
i++;
}
return null;
}
测试结果
2.7、插入节点方法
代码逻辑步骤
- 如果索引是0那么直接调用头插法添加方法。
- 找到上一个方法node节点。
- 把上个节点的指向节点换成当前添加节点。当前添加节点的指向节点换成上个节点的指向节点。
/**
* 插入节点方法
*
* @param value 插入的值
* @param index 插入的位置
* @return
*/
public Boolean insert(int value, int index) {
//1.如果索引是0那么直接调用头插法添加方法即可
if (index == 0) {
return addFirst(value);
}
//2、先找到上一个方法node节点。,招不到就抛异常。
Node findNode = findNode(index - 1);
if (null == findNode) {
throw new IllegalArgumentException("索引位置不正确!");
}
//3、把上个节点的指向节点换成当前添加节点。当前添加节点的指向节点换成上个节点的指向节点。
findNode.next = new Node(value, findNode.next);
return true;
}
/**
* 根据索引获取Node。
*
* @param index
* @return
*/
private Node findNode(int index) {
//索引从0开始
int i = 0;
Node node = head;
while (null != node) {
//如果下标值相等。则返回node.否则循环继续。
if (i == index) {
return node;
}
node = node.next;
i++;
}
return null;
}
运行结果
2.8、删除节点方法
- 如果索引是0,直接将head的指向节点替换成head节点;
- 找到删除节点和删除节点的上一个节点。
- 将删除节点的指向节点赋值给上一个节点的指向节点。
/**
* 删除节点方法
*
* @param index 删除节点的索引
* @return
*/
public Boolean remove(int index) {
//1.如果索引是0,直接将head的指向节点替换成head节点;
if (index == 0) {
head = head.next;
return true;
}
//2、找到当前节点(未找到也相当于删除了)
Node removeNode = findNode(index);
if (null != removeNode) {
//3、找到当前节点的前一个节点
Node frontNode = findNode(index - 1);
//4、将删除节点的指向节点赋值给前一个节点的指向节点。
frontNode.next = removeNode.next;
}
return true;
}
/**
* 根据索引获取Node。
*
* @param index
* @return
*/
private Node findNode(int index) {
//索引从0开始
int i = 0;
Node node = head;
while (null != node) {
//如果下标值相等。则返回value.否则循环继续。
if (i == index) {
return node;
}
node = node.next;
i++;
}
return null;
}
测试结果;删除索引:2
2.8、哨兵模式
1、说明
在Java编程中,尤其是在实现链表数据结构时,哨兵模式的运用体现在为链表添加一个特殊用途的节点,通常称为“哨兵节点”或“哑元节点”。它的作用主要包括:
1、简化边界条件处理:
在单链表和双向链表中,头哨兵节点作为列表的第一个元素,其主要作用是在进行插入、删除等操作时提供一个固定的参照点。例如,当需要删除链表头部元素时,无需检查链表是否为空,直接对哨兵节点的下一个节点进行操作即可。
双向链表中的尾哨兵节点同样简化了对链表尾部的操作,如在尾部添加新元素时,可以直接将新元素与尾哨兵节点连接。
2、防止空指针异常:
当链表为空时,通过设置哨兵节点可以避免在遍历或修改链表时遇到null引发的空指针异常。
3、提高算法效率:
由于不需要每次都检查链表边界,所以使用哨兵节点能够降低算法实现的复杂度,并可能提高程序运行效率。
4、标准化操作流程:
哨兵节点的存在使链表操作逻辑更加一致,无论是处理非空链表还是空链表,都可以采用相同的代码逻辑来完成任务。
2、将单向链表改成哨兵模式
1、头部节点设置默认值。
// 头部节点
private Node head=new Node(-1,null);
2、添加节点(尾插法)去掉头节点为null 判断
/**
* 添加节点(尾插法)
*
* @return
*/
public Boolean addLast(int value) {
Node last = head;
while (null != last.next) {
last = last.next;
}
last.next = new Node(value, null);
return true;
}
3、获取节点值从-1开始
3、根据索引获取Node也要从-1开始
5、遍历节点要从head指向节点开始
6、插入节点方法,去掉索引等于0判断
7、 删除节点方法,去掉索引等于0的判断。
8、 添加节点(头插法),改成直接用插入
三、双向链表(哨兵模式)
3.1、基本概念
双向链表(哨兵模式)是一种在双向链表数据结构中引入额外特殊节点的设计方法,通常包括一个头哨兵节点和一个尾哨兵节点。这种模式旨在简化对链表边界条件的处理以及优化插入、删除等操作。
基本概念与结构定义:
- 双向链表:
双向链表是一种线性数据结构,其中每个元素(节点)包含两个指针,分别指向其前节点和后节点。这使得从任意节点出发都能进行正向或反向遍历。
- 哨兵节点:
在双向链表(哨兵模式)中,我们添加了两个特殊的哨兵节点——头哨兵节点和尾哨兵节点。
头哨兵节点:作为链表的逻辑起点,其next指针指向第一个实际数据节点,而prev指针始终指向尾哨兵节点(或为null)。即使链表为空,头哨兵节点也始终存在,为插入和查找提供了一个固定的参考点。
尾哨兵节点:作为链表的逻辑终点,其prev指针指向最后一个实际数据节点,而next指针回指到头哨兵节点(或为null)。这样无论何时,都有明确的尾节点供操作使用。
- 优点与作用:
1、空链表的表示统一且清晰,无需对空链表进行特殊判断。
2、插入操作简化,无论是头部插入还是尾部插入新节点,只需调整相应哨兵节点与新节点以及原首尾节点之间的连接关系即可,无需考虑复杂的边界条件。
3、删除操作同样简便,特别是在处理尾部元素时,由于尾哨兵的存在,可以轻松定位并删除末尾节点。
4、遍历更加方便,始终可以从头哨兵开始,通过next指针进行正向遍历,或通过prev指针进行反向遍历。
3.2、节点定义
public class DoublyLinkedListSentinel {
private final Node head;//头部节点
private final Node tail; //尾部节点
/**
* 头部节点和尾部节点设置默认值。
*/
public DoublyLinkedListSentinel() {
head = new Node(null, -1, null);//设置头部节点默认值
tail = new Node(null, -2, null);//设置尾部节点默认值
head.next = tail;//设置头部节点的后节点为尾节点
tail.prev = head;//设置尾部节点的前节点为头部节点
}
static class Node {
public Node prev;//前节点
public int value; //节点值
public Node next; //后节点
/**
* @param prev 前节点
* @param value 节点值
* @param next 后节点
*/
public Node(Node prev, int value, Node next) {
this.prev = prev;
this.value = value;
this.next = next;
}
}
}
3.2、添加值方法(头插法)
/**
* 添加值(头插法)
*
* @param value
* @return
*/
public boolean addFirst(int value) {
Node next = head.next;//1、获取头部节点的后节点
Node node = new Node(head, value, next);//创建添加节点,设置添加节点的前节点和后节点
head.next = node;//设置头节点的后节点为添加节点
next.prev = node;//设置头节点的后节点的前节点为添加节点。
//insert(0, value);//也可以直接调用插入方法
return true;
}
运行结果
3.3、添加值方法(头插法)
/**
* 添加值(尾插法)
*
* @param value 添加的值
* @return
*/
public Boolean addLast(int value) {
Node prev = tail.prev; //1、获取尾部节点的前节点
Node addNode = new Node(prev, value, tail);//2、创建添加节点,设置前节点和后节点
prev.next = addNode; //3、设置前节点的后节点为添加节点
tail.prev = addNode;//4、设置尾部节点的前节点为添加节点
return true;
}
运行结果
3.4、查找节点值方法
/**
* 查找节点值。
*
* @param index 查找索引
* @return
*/
public Integer get(int index) {
Node node = findNode(index);
if (null != node) {
return node.value;
}
return null;
}
/**
* 查找节点
*
* @param index 查找索引
* @return
*/
private Node findNode(int index) {
int i = -1;
Node findNode = head;//从头节点开始
while (findNode != tail) {//如果查询节点不是尾节点循环继续
if (i == index) {
return findNode;
}
findNode = findNode.next;
i++;
}
return null;
}
运行结果
3.5、插入节点方法
/**
* 插入节点
*
* @param index 插入索引位置
* @param value 插入的值
*/
public boolean insert(int index, int value) {
//1、获取插入索引位置的前节点
Node prevNode = findNode(index - 1);
if (null == prevNode) {
throw new IllegalArgumentException("索引位置:[" + index + "]不合法!");
}
Node nextNode = prevNode.next; //2、获取插入索引位置的后节点(前节点指向的后节点就是插入节点的后节点)
Node node = new Node(prevNode, value, nextNode); //3、创建插入节点,并且设置插入节点的前节点和后节点
prevNode.next = node; //4、修改前节点的后节点为插入节点
nextNode.prev = node; //5、修改后节点的前节点为插入节点
return true;
}
/**
* 查找节点
*
* @param index 查找索引
* @return
*/
private Node findNode(int index) {
int i = -1;
Node findNode = head;//从头节点开始
while (findNode != tail) {//如果查询节点不是尾节点循环继续
if (i == index) {
return findNode;
}
findNode = findNode.next;
i++;
}
return null;
}
运行结果
3.6、删除节点方法
/**
* 删除节点
*
* @param index 索引
* @return
*/
public boolean remove(int index) {
Node prev = findNode(index - 1); //1、查找删除节点的前节点
if (null == prev) { //2、判断删除节点不能是头节点
throw new IllegalArgumentException("索引位置:[" + index + "]不合法!");
}
Node removeNode = prev.next;
if (removeNode == tail) {//3、判断删除节点不能是尾节点
throw new IllegalArgumentException("索引位置:[" + index + "]不合法!");
}
Node next = removeNode.next; //4、获取删除节点的后节点
prev.next = next;//5、设置删除节点的前节点的后节点为删除节点的后节点。
next.prev = prev;//6、设置删除节点的后节点的前节点为删除节点的前节点。
return true;
}
/**
* 查找节点
*
* @param index 查找索引
* @return
*/
private Node findNode(int index) {
int i = -1;
Node findNode = head;//从头节点开始
while (findNode != tail) {//如果查询节点不是尾节点循环继续
if (i == index) {
return findNode;
}
findNode = findNode.next;
i++;
}
return null;
}
运行结果
3.7、遍历节点方法
/**
* 循环遍历
*/
public void circulate() {
Node node = head.next;
while (node != tail) {//如果节点不是尾节点,循环继续
System.out.println("打印值:" + node.value);
node = node.next;
}
}
三、循环链表或环形链表(哨兵模式)
3.1、说明
双向循环链表(哨兵模式)是一种改进的双向循环链表实现方式,其中引入了一个特殊的“哨兵”节点。哨兵节点不存储实际数据,它的主要作用是简化空链表和非空链表的处理逻辑,以及在进行插入、删除等操作时提供一个固定不变的参考点。
结构定义:
- 哨兵节点是一个额外创建的节点,它位于双向循环链表的开始和结束位置。
- 哨兵节点的数据域可以设置为特殊值或NULL,以表示它是哨兵而非有效数据节点。
- 哨兵节点的前驱指针prev指向自身,后继指针next同样指向自身,当链表为空时,整个链表仅包含这个哨兵节点形成的环。
特性:
- 始终存在头尾节点:由于有了哨兵的存在,无论链表是否为空,都有明确的头节点(也是尾节点)可以引用,这使得代码逻辑更加简洁统一。
- 插入与删除操作优化:对于插入操作,在头部或尾部插入新节点时,只需要调整哨兵节点的前后指针即可;而对于其他位置插入,可直接通过哨兵节点作为起点来定位插入位置,无需检查边界条件。
- 遍历便利性:从哨兵节点出发,无论是正向还是反向遍历都更为方便,因为不需要额外判断链表是否为空。
使用哨兵模式的双向循环链表在很多情况下能够提高算法效率和代码可读性,尤其是在需要频繁执行插入、删除等操作的情况下。
3.2、节点定义
注意:由于是循环链表。所以不存在空节点。所以只要有一个哨兵节点即可。
/**
* 双向循环链表(哨兵模式)
*/
public class DoublyLinkedListSentinel2 {
//哨兵
private final Node sentinel = new Node(null, -1, null);
/**
* 设置哨兵节点前后节点值。
*/
public DoublyLinkedListSentinel2() {
sentinel.prev = sentinel;
sentinel.next = sentinel;
}
/**
* node内部类。
*/
static class Node {
public Node prev;//前节点
public int value; //节点值
public Node next; //后节点
/**
* @param prev 前节点
* @param value 节点值
* @param next 后节点
*/
public Node(Node prev, int value, Node next) {
this.prev = prev;
this.value = value;
this.next = next;
}
}
}
3.3、添加节点方法(头插法)
/**
* 添加值(头插法)
*
* @param value
* @return
*/
public boolean addFirst(int value) {
Node next = sentinel.next;//获取哨兵节点的后节点
Node node = new Node(sentinel, value, next);//创建新节点,将哨兵节点当作添加节点的前节点,后节点设置为哨兵节点的后节点。
sentinel.next = node; //设置哨兵节点的后节点为新节点
next.prev = node; //设置哨兵节点后节点的前节点为新节点
return true;
}
运行结果
3.3、添加节点方法(尾插法)
/**
* 添加值(尾插法)
*
* @param value 添加的值
* @return
*/
public Boolean addLast(int value) {
Node prev = sentinel.prev;//获取哨兵节点的前节点
Node node = new Node(prev, value, sentinel);//创建新节点,将哨兵节点的前节点当作添加节点的前节点,哨兵节点设置为后节点。
prev.next = node;//新节点设置为哨兵节点的前节点的后节点
sentinel.prev = node;//新节点设置为哨兵节点的前节点
return true;
}
运行结果
3.4、查找节点方法
/**
* 查找节点值。
*
* @param value 节点值
* @return
*/
public Integer get(int value) {
Node node = findNodeByValue(value);
if (null != node) {
return node.value;
}
return null;
}
/**
* 查找节点
*
* @param value 查找节点值
* @return
*/
private Node findNodeByValue(int value) {
Node findNode = sentinel.next;//从哨兵节点的后节点开始
while (findNode != sentinel) {//如果查询节点不是哨兵节点循环继续
if (findNode.value == value) {
return findNode;
}
findNode = findNode.next;
}
return null;
}
运行结果
3.4、删除节点方法
/**
* 根据节点值删除节点
*
* @param value 节点值
* @return
*/
public boolean removeByValue(int value) {
Node node = findNodeByValue(value);//获取需要删除节点
if (null != node) {
Node prev = node.prev;//获取删除节点的前节点
Node next = node.next;//获取删除节点的后节点
prev.next = next;
next.prev = prev;
}
return true;
}
/**
* 查找节点
*
* @param value 查找节点值
* @return
*/
private Node findNodeByValue(int value) {
Node findNode = sentinel.next;//从哨兵节点的后节点开始
while (findNode != sentinel) {//如果查询节点不是哨兵节点循环继续
if (findNode.value == value) {
return findNode;
}
findNode = findNode.next;
}
return null;
}
运行结果
3.4、遍历节点方法
/**
* 循环遍历
*/
public void circulate() {
Node node = sentinel.next;
while (node != sentinel) {//如果节点不哨兵尾节点,循环继续
System.out.println("打印值:" + node.value);
node = node.next;
}
}
3.5、递归遍历
/**
* 遍历
*/
public void circulate2() {
recursion(sentinel.next);
}
/**
* 递归遍历
*/
public void recursion(Node node) {
if (node != sentinel) {
System.out.println("打印值:" + node.value);
recursion(node.next);
}
}
运行结果: