3.1 链表(Linked List)
3.1.1 基本介绍
链表(Linked list)是一种常见的基础数据结构,是一种线性表,但是并不会按线性的顺序存储数据,而是按照链式存储的方式存储数据,在每一个节点里存到下一个节点的指针(Pointer)。由于不必须按顺序存储,链表在插入的时候可以达到O(1)的复杂度,比另一种线性表顺序表快得多,但是查找一个节点或者访问特定编号的节点则需要O(n)的时间,而顺序表相应的时间复杂度分别是O(logn)和O(1)。
使用链表结构可以克服数组链表需要预先知道数据大小的缺点,链表结构可以充分利用计算机内存空间,实现灵活的内存动态管理。但是链表失去了数组随机读取的优点,同时链表由于增加了结点的指针域,空间开销比较大。
链表作为一种基础的数据结构可以用来生成其它类型的数据结构。链表通常由一连串节点组成,每个节点包含任意的实例数据(data fields)和一或两个用来指向上一个/或下一个节点的位置的链接(“links”)。链表最明显的好处就是,链表是一种自我指示数据类型,因为它包含指向另一个相同类型的数据的指针(链接)。链表允许插入和移除表上任意位置上的节点,但是不允许随机存取。链表有很多种不同的类型:单向链表,双向链表以及循环链表。
3.1.2 单链表的应用实例
使用带 head 头的单向链表实现 增删改查操作
添加
-
创建一个 head 头节点,表示单链表的头信息。
-
添加每一个节点时,直接添加到链表的最后节点。
有序添加 -
通过遍历链表和辅助指针,找到新添加的节点的位置。
-
新添加节点的next指针 newNode.next = temp.next
-
temp节点的next指针 temp.next = newNode
修改 -
通过遍历链表找到要修改的节点,然后修改可变的值
删除
-
通过遍历链表找到要删除的这个节点的前一个节点temp。
-
修改temp节点的next temp.next = temp.next.next。
-
被删除的节点没有任何引用,会被垃圾回收机制回收。
列表 -
通过辅助指针,遍历链表。
3.1.3 单链表的代码实现
public class SingleLinkedListTest {
public static void main(String[] args) {
// 先创建节点,
HeroNode hero1 = new HeroNode(1, "宋江", "及时雨");
HeroNode hero2 = new HeroNode(2, "卢俊义", "玉麒麟");
HeroNode hero3 = new HeroNode(3, "吴用", "智多星");
HeroNode hero4 = new HeroNode(4, "林冲", "豹子头");
// 创建链表
SingleLinkedList singleLinkedList = new SingleLinkedList();
// 加入元素
// singleLinkedList.add(hero1);
// singleLinkedList.add(hero4);
// singleLinkedList.add(hero2);
// singleLinkedList.add(hero3);
// 加入按照编号的顺序
singleLinkedList.addByOrder(hero4);
singleLinkedList.addByOrder(hero1);
singleLinkedList.addByOrder(hero2);
singleLinkedList.addByOrder(hero3);
// 显示列表
singleLinkedList.list();
// 测试修改节点的代码
HeroNode newHeroNode = new HeroNode(2, "小卢", "玉麒麟~~");
singleLinkedList.update(newHeroNode);
System.out.println("修改后的链表情况~~");
singleLinkedList.list();
// 删除一个节点
singleLinkedList.delete(newHeroNode);
System.out.println("删除后的链表情况~~");
singleLinkedList.list();
}
}
class SingleLinkedList {
private HeroNode first = new HeroNode(0, "", "");
public void add(HeroNode e) {
HeroNode temp = first;
// 遍历链表,找到链表的最后节点
while (true) {
if (temp.next == null) {
break;
}
// 如果没有找到最后,将将 temp 后移
temp = temp.next;
}
// 当退出 while 循环时,temp 就指向了链表的最后,将最后这个节点的 next 指向新的节点
temp.next = e;
}
public void addByOrder(HeroNode e) {
HeroNode temp = first;
boolean flag = false;
// 遍历链表,找到链表的最后节点
while (true) {
if (temp.next == null) {
break;
}
if (temp.next.order > e.order) {
// 位置找到,就在 temp 的后面插入
break;
} else if (temp.next.order == e.order) {
// 说明希望添加的 heroNode 的编号已然存在
flag = true;
break;
}
// 如果没有找到最后,将将 temp 后移
temp = temp.next;
}
// 当退出 while 循环时,temp 就指向了链表的最后,判断 flag 的值
if (flag) {
System.out.printf("准备插入的英雄的编号 %d 已经存在了, 不能加入\n", e.order);
} else {
// 插入到链表中,temp 的后面
e.next = temp.next;
temp.next = e;
}
}
public void update(HeroNode e) {
HeroNode temp = first;
boolean flag = false;
if (null == temp.next) {
System.out.println("链表为空~");
return;
}
while (true) {
// 已经遍历完列表
if (null == temp) {
break;
}
if (temp.order == e.order) {
flag = true;
break;
}
temp = temp.next;
}
if (flag) {
temp.name = e.name;
temp.nickname = e.nickname;
} else {
// 没有找到
System.out.printf("没有找到编号 %d 的节点,不能修改\n", e.order);
}
}
public void delete(HeroNode e) {
HeroNode temp = first;
boolean flag = false;
if (null == temp.next) {
System.out.println("链表为空~");
return;
}
while (true) {
// 已经遍历完列表
if (null == temp) {
break;
}
if (temp.next.order == e.order) {
// 找到的待删除节点的前一个节点 temp
flag = true;
break;
}
temp = temp.next;
}
if (flag) {
temp.next = temp.next.next;
} else {
// 没有找到
System.out.printf("没有找到编号 %d 的节点,不能修改\n", e.order);
}
}
public void list() {
HeroNode temp = first.next;
if (null == temp) {
System.out.println("链表为空~");
return;
}
while (true) {
if (null == temp) {
return;
}
System.out.println(temp);
temp = temp.next;
}
}
}
@Data
class HeroNode {
protected int order;
protected String name;
protected String nickname;
protected HeroNode next;
public HeroNode(int order, String name, String nickname) {
this.order = order;
this.name = name;
this.nickname = nickname;
}
}
3.1.4 单链表的常见问题
问题:获取单链表中有效的节点的个数
方案:
public int size(HeroNode first) {
int size = 0;
// 定义辅助指针
HeroNode temp = first.next;
while (null != temp) {
size++;
temp = temp.next;
}
return size;
}
问题:获取单链表中倒数第k个节点
方案:
- index 表示是倒数第 index 个节点。
- 首先判断链表是否为空,入参是否合法。
- 然后获取链表的size。
- 在得到size后,从链表的第一个开始遍历(size-index)个,存在就返回对应节点,否则就返回null。
public HeroNode findLastIndexNode(HeroNode first, int index) {
HeroNode temp = first.next;
if (null == temp) {
return null;
}
int size = size(first);
if (index <= 0 || index > size) {
return null;
}
for (int i = 0; i < size - index; i++) {
temp = temp.next;
}
return temp;
}
问题:实现单链表的反转
方案:
- 定义临时的反转头节点 HeroNode reversetNode = new HeroNode(0, “”, “”)。
- 遍历链表将每个节点取出,然后插入到反转头节点的最前端。
- 修改头节点的next:first.next = reversetNode.next。
public void reverseLinkedList(HeroNode first) {
HeroNode temp = first.next;
// 如果链表为空或者链表只有一个节点,之际返回
if (null == temp || null == temp.next) {
return;
}
// 定义临时的反转头节点
HeroNode reversetNode = new HeroNode(0, "", "");
// 定义临时next节点
HeroNode next;
// 遍历链表,然后将每个节点放到新链表的最前端
while (null != temp) {
next = temp.next;
temp.next = reversetNode.next;
reversetNode.next = temp;
temp = next;
}
first.next = reversetNode.next;
}
问题:反向遍历单链表
方案:
- 可以先将单链表进行反转,然后遍历单链表。
- 使用栈数据结构,将节点压入栈中,利用栈先进后出的特点,实现反向遍历单链表。
public void reversePrint() {
HeroNode temp = first.next;
// 链表为空
if (null == temp) {
return;
}
// 创建栈数据结构
Stack<HeroNode> stack = new Stack<>();
// 遍历链表,将每个节点压入到栈中
while (null != temp) {
stack.push(temp);
temp = temp.next;
}
// 将栈中的节点进行打印,pop 出栈
while (stack.size() > 0) {
// stack 的特点是先进后出
System.out.println(stack.pop());
}
}
问题:合并两个有序的单链表,合并之后依然有序
方案:
3.2.1 双向链表的应用实例
管理单向链表的缺点分析:
- 单向链表的查找方向只能是一个方向,而双向链表可以向前或者向后查找。
- 单向链表不能自我删除,需要靠辅助节点,而双向链表则可以自我删除。
添加
-
创建一个 head 头节点,表示双向链表的头信息。
-
添加每一个节点时,直接添加到链表的最后节点。
-
temp.next = newNode 且 newNode.pre = temp。
修改 -
通过遍历链表找到要修改的节点,然后修改可变的值。
删除
-
通过遍历链表找到要删除的这个节点 temp。
-
修改temp节点 temp.pre.next = temp.next 且 temp.next.pre = temp.pre。
-
被删除的节点没有任何引用,会被垃圾回收机制回收。
列表 -
通过辅助指针,可以向前或者向后遍历链表。
3.2.2 双向链表的代码实现
public class DoubleLinkedListTest {
public static void main(String[] args) {
// 先创建节点
DoubleHeroNode hero1 = new DoubleHeroNode(1, "宋江", "及时雨");
DoubleHeroNode hero2 = new DoubleHeroNode(2, "卢俊义", "玉麒麟");
DoubleHeroNode hero3 = new DoubleHeroNode(3, "吴用", "智多星");
DoubleHeroNode hero4 = new DoubleHeroNode(4, "林冲", "豹子头");
// 创建一个双向链表
DoubleLinkedList doubleLinkedList = new DoubleLinkedList();
// 添加
doubleLinkedList.addByOrder(hero1);
doubleLinkedList.addByOrder(hero4);
doubleLinkedList.addByOrder(hero3);
doubleLinkedList.addByOrder(hero2);
doubleLinkedList.list();
System.out.println(doubleLinkedList.size());
// 修改
DoubleHeroNode newHeroNode = new DoubleHeroNode(4, "公孙胜", "入云龙");
doubleLinkedList.update(newHeroNode);
System.out.println("修改后的链表情况");
doubleLinkedList.list();
System.out.println(doubleLinkedList.size());
// 删除
doubleLinkedList.delete(hero4);
System.out.println("删除后的链表情况~~");
doubleLinkedList.list();
System.out.println(doubleLinkedList.size());
}
}
class DoubleLinkedList {
DoubleHeroNode first = new DoubleHeroNode(0, "", "");
public void add(DoubleHeroNode e) {
DoubleHeroNode temp = first;
// 遍历链表,找到链表的最后节点
while (true) {
if (temp.next == null) {
break;
}
// 如果没有找到最后,将将 temp 后移
temp = temp.next;
}
// 当退出 while 循环时,temp 就指向了链表的最后,将最后这个节点的 next 指向新的节点,新节点的 pre 指向temp
temp.next = e;
e.pre = temp;
}
public void addByOrder(DoubleHeroNode e) {
DoubleHeroNode temp = first;
boolean flag = false;
// 遍历链表,找到链表对应的节点
while (true) {
if (temp.next == null) {
break;
}
if (temp.next.order > e.order) {
// 位置找到,就在 temp 的后面插入
break;
} else if (temp.next.order == e.order) {
// 说明希望添加的 heroNode 的编号已然存在
flag = true;
break;
}
// 如果没有找到最后,将将 temp 后移
temp = temp.next;
}
// 当退出 while 循环时,temp 就指向了链表的最后,判断 flag 的值
if (flag) {
System.out.printf("准备插入的英雄的编号 %d 已经存在了, 不能加入\n", e.order);
} else {
// 插入到链表中,temp 的后面
e.next = temp.next;
e.pre = temp;
if (null != temp.next) {
temp.next.pre = e;
}
temp.next = e;
}
}
public void update(DoubleHeroNode e) {
DoubleHeroNode temp = first;
boolean flag = false;
if (null == temp.next) {
System.out.println("链表为空~");
return;
}
while (true) {
// 已经遍历完列表
if (null == temp) {
break;
}
if (temp.order == e.order) {
flag = true;
break;
}
temp = temp.next;
}
if (flag) {
temp.name = e.name;
temp.nickname = e.nickname;
} else {
// 没有找到
System.out.printf("没有找到编号 %d 的节点,不能修改\n", e.order);
}
}
public void delete(DoubleHeroNode e) {
DoubleHeroNode temp = first;
boolean flag = false;
if (null == temp.next) {
System.out.println("链表为空~");
return;
}
while (true) {
// 已经遍历完列表
if (null == temp) {
break;
}
if (temp.order == e.order) {
// 找到的待删除节点 temp
flag = true;
break;
}
temp = temp.next;
}
if (flag) {
temp.pre.next = temp.next;
if (null != temp.next) {
temp.next.pre = temp.pre;
}
} else {
// 没有找到
System.out.printf("没有找到编号 %d 的节点,不能修改\n", e.order);
}
}
public void list() {
DoubleHeroNode temp = first.next;
if (null == temp) {
System.out.println("链表为空~");
return;
}
while (true) {
if (null == temp) {
return;
}
System.out.println(temp);
temp = temp.next;
}
}
public int size() {
int size = 0;
// 定义辅助指针
DoubleHeroNode temp = first.next;
while (null != temp) {
size++;
temp = temp.next;
}
return size;
}
}
class DoubleHeroNode {
protected int order;
protected String name;
protected String nickname;
protected DoubleHeroNode pre;
protected DoubleHeroNode next;
public DoubleHeroNode(int order, String name, String nickname) {
this.order = order;
this.name = name;
this.nickname = nickname;
}
@Override
public String toString() {
return "HeroNode [order = " + order + ", name = " + name + ", nickname = " + nickname + "]";
}
}
3.3.1 单向环形链表
在一个循环链表中,首节点和末节点被连接在一起,这种方式在单向和双向链表中皆可实现。要转换一个循环链表,可以开始于任意一个节点然后沿着列表的一个方向直到返回开始的节点,循环链表可以被视为“无头无尾”,这种列表很利于节约数据存储缓存,指向整个列表的指针可以被称作访问指针。
3.3.2 单向环形链表的应用实例
约瑟夫问题
设编号为 1,2,… n 的 n 个人围坐一圈,约定编号为 k(1<=k<=n)的人从 1 开始报数,数到 m 的那个人出列,它的下一位又从 1 开始报数,数到 m 的那个人又出列,依次类推,直到所有人出列为止,由此产生一个出队编号的序列。
思路分析
使用循环链表来处理约瑟夫问题,先构成一个有 n 个节点的单向循环链表,然后由 k 节点开始从 1 开始计数,记到 m 时对应节点从链表删除,然后再从被删除节点的下一个节点开始从 1 计数,知道所有的节点从链表中删除。
- 首先创建第一个节点 first。
- 然后创建一个节点,让 fiirst 指向该节点,并形成环形单向链表。
- 后面每创建一个节点,就把该节点加入到环形链表。
- 定义辅助指针 current 指向 first 节点。
- 遍历该环形单向链表,current.next = first 结束。
3.3.3 约瑟夫代码的实现
public class CircleSingleLinkedListTest {
public static void main(String[] args) {
CircleSingleLinkedList linkedList = new CircleSingleLinkedList();
linkedList.add(new Node(1));
linkedList.add(new Node(2));
linkedList.add(new Node(3));
linkedList.add(new Node(4));
linkedList.add(new Node(5));
linkedList.list();
System.out.printf("链表的 size 为 %d \n\n", linkedList.size());
System.out.println("约瑟夫问题的代码实现:");
linkedList.countNode(linkedList.size(), 2, 1);
}
}
class CircleSingleLinkedList {
// 创建第一个节点,当前没有编号
private Node first = null;
// 辅助指针
private Node current = null;
public void add(Node e) {
if (null == first) {
first = e;
// 形成环形链表
first.next = first;
// current 指向 first
current = first;
} else {
current.next = e;
e.next = first;
current = e;
}
}
/**
* 约瑟夫问题
*
* @param n 单向环形链表共有 n 个节点
* @param k 由 k 节点开始计数
* @param m 每次数 m
*/
public void countNode(int n, int k, int m) {
// 参数校验
if (null == first || k < 1 || n < k) {
System.out.println("参数输入有误, 请重新输入");
return;
}
// 定义辅助指针,并指向环形链表的最后一个位置
Node helpNode = first;
while (true) {
// 说明 helpNode 节点已经指向最后的节点
if (helpNode.next == first) {
break;
}
helpNode = helpNode.next;
}
// 报数前将 first 和 helpNode 移动 k - 1 次
for (int j = 0; j < k - 1; j++) {
first = first.next;
helpNode = helpNode.next;
}
// 循环删除符合条件的节点
while (true) {
// 说明环形链表中只有一个节点
if (helpNode == first) {
break;
}
// 将 first 和 helpNode 指针同时移动 m - 1
for (int j = 0; j < m - 1; j++) {
first = first.next;
helpNode = helpNode.next;
}
// 这时 first 指向的节点,就是要删除的节点
System.out.printf("编号 %d 删除\n", first.number);
// 删除对应的节点
first = first.next;
helpNode.next = first;
}
System.out.printf("编号 %d 删除\n", first.number);
}
public int size() {
int size = 0;
if (null == first) {
return size;
}
Node helpNode = first;
while (true) {
if (helpNode.next == first) {
break;
}
size++;
helpNode = helpNode.next;
}
return size;
}
public void list() {
Node helpNode = first;
if (null == helpNode) {
System.out.println("链表为空~~");
return;
}
while (true) {
System.out.printf("编号 %d \n", helpNode.number);
if (helpNode.next == first) {
break;
}
helpNode = helpNode.next;
}
}
}
@Getter
@Setter
class Node {
protected int number;
protected Node next;
public Node(int number) {
this.number = number;
}
}