目录
一、链表知识
1.1 链表定义
由指针链接n 个结点组成一个链表。它是线性表的链式存储映像,称为线性表的链式存储结构。
结点:数据元素的存储映像,映射。每个节点由数据域和指针域两部分组成。
数据域:存储元素数据,即当前节点的存储数据
指针域:存储下一个节点的存储位置,即指向下一个节点的指针
1.2 链表结构
链表的元素是节点,由一个或多个结点组成。节点包括两个元素:数据域和指针域。
简单理解的话,类似链条,一节一节相连,每个链节都有自己的零件(数据)并且链接下一个链节(指针)。
自行车的车链是一个环状的链子,或者说是首位相连的,这是链表中环结构下面会有说明。
我们可以从图1-1 链表中抽象出链表如图1-2 所示,从首节点headNode 依此指向下一个node,直至尾节点tailNode
进而具体到单向链表
- 初始化指针指向首节点headNode,初始化指针的next 为null 即链表为空
- 每一个节点node 都由数据域和指针域组成,数据域存放当前节点的数据,指针域存放当前指针的下一个指向节点
- 尾节点tailNode 的指针为空
1.3 优点
动态存储形式,数据元素可自由扩充;
在物理存储空间上并不一定相连,所以链表的插入、删除动作高效,不需要像数组、顺序表那样移动过多的元素。
1.4 单链表、双链表、循环链表拓展
常见链表有单向链表和双向链表,单向链表是只有一个方向,从链表的首节点headNode 一直指向尾节点tailNode。双向链表是链表即可从headNode 依此指向tailNode 又可反向从tailNode 依此指向heaNode。另外循环链表是链表的尾节点直接指向头节点,即tailNode.next = headNode
单链表、双链表、循环链表:
结点只有一个指针域的链表,称为单链表或线性链表
有两个指针域的链表,称为双链表
首尾相接的链表称为循环链表
1.5 本文说明
本文涉及到链表的多个常用方法,包括链表的初始化、添加节点、删除节点、查询节点、链表的逆置、模拟链表的环结构、判断链表是否存在环结构、寻找环的入口等方法。关于链表的相交问题暂不涉及。
此处以单向链表为例,面向对象分析链表结构,将节点对象化,链表对象化,此处使用Java 语言演示。
二、面向对象分析链表
2.1 节点封装类Node.java
有两个属性,当前节点存储的数据Object data,指向下一节点的指针Node next。
/**
* @Description :节点类
* @Author: niaonao
* @Date: 2018/8/11 13:07
*/
public class Node {
//当前节点的数据
Object data;
//当前节点的下一个节点
Node next;
public Object getData() { return data; }
public void setData(Object data) { this.data = data; }
public Node getNext() { return next; }
public void setNext(Node next) { this.next = next; }
/**
* 无参构造函数
*/
public Node() { next = null; }
/**
* 带参构造函数
* @param data
*/
public Node(Object data) {
this.data = data;
next = null;
}
}
2.2 链表封装类ChainTable.java
链表是由一个或多个节点组成的。定义如下封装类:
/**
* 链表
* 由一个或多个节点组成
* 每一个节点存放下一个元素, 通过首节点即可获取链表全部节点元素, 因此可以将首节点作为链表对象
* 节点:
* 每个节点包含两个元素: 数据对象data 和下一个节点对象next
* 通过data 存储当前节点的数据
* 通过next 存储下一个节点
*
* @Description :链表类
* @Author: niaonao
* @Date: 2018/8/11 13:07
*/
public class ChainTable {
//声明链表
private Node chain;
/**
* 构造函数
* 初始化链表, 此时chain.next == null, 即链表为空
*/
public ChainTable() {
chain = new Node();
}
//链表方法待补充
}
下面关于封装单链表的常用方法包括插入首节点,插入尾节点,指定位置插入节点,删除指定位置节点等。
方法不一一分开介绍了,其中的方法都有注释说明。
基本方法:
- void insert(Object val) 插入链表元素的方法(插入链表首位, 插入链表尾部, 插入链表指定位置)
- int getLength() 获取链表长度的方法
- Node getPosition(int position) 获取链表指定位置元素(正序获取, 逆序获取)
- boolean judgeLoop() 判断链表是否有环的方法
- int getLoopLength() 获取环的长度
- Node entryLoop() 查找环入口的方法
- Node reverse() 逆置链表的方法
- void clear() 清除链表
/**
* 链表
* 由一个或多个节点组成
* 每一个节点存放下一个元素, 通过首节点即可获取链表全部节点元素, 因此可以将首节点作为链表对象
* 节点:
* 每个节点包含两个元素: 数据对象data 和下一个节点对象next
* 通过data 存储当前节点的数据
* 通过next 存储下一个节点
* 基本方法:
* void insert(Object val) 插入链表元素的方法(插入链表首位, 插入链表尾部, 插入链表指定位置)
* int getLength() 获取链表长度的方法
* Node getPosition(int position) 获取链表指定位置元素(正序获取, 逆序获取)
* boolean judgeLoop() 判断链表是否有环的方法
* int getLoopLength() 获取环的长度
* Node entryLoop() 查找环入口的方法
* Node reverse() 逆置链表的方法
* void clear() 清除链表
*
* @Description :链表类
* @Author: niaonao
* @Date: 2018/8/11 13:07
*/
public class ChainTable {
//声明链表
private Node chain;
/**
* 构造函数
*/
public ChainTable() {
chain = new Node();
}
/**
* 获取链表的长度
*
* @return
*/
public int getLength() {
//遍历计算链表长度,当前节点的next 下一个节点不为null, 节点数自增一即链表长度自增一
int count = 0;
Node cursor = chain;
while (cursor.next != null) {
cursor = cursor.next;
count++;
}
return count;
}
/**
* 在链表首部添加节点
*/
public void insertHead(Object val) {
//新建链表节点,下一节点指向链表的首位,然后更新链表首位为此节点。
Node head = new Node(val);
head.next = chain.next;
chain.next = head;
}
/**
* 在链表尾部添加节点
*
* @param val
*/
public void insertTail(Object val) {
//新建链表节点node,当cursor.next为null即cursor已经在链表尾部
Node tail = new Node(val);
Node cursor = chain;
while (cursor.next != null) {
cursor = cursor.next;
}
//获取到最后一个节点,使其next 指向tail 节点
cursor.next = tail;
}
/**
* 链表下表从1 开始,在指定位置插入节点
*
* @param val
* @param position
*/
public void insert(Object val, int position) {
// 新建节点, 遍历获取链表的第port 个节点
Node Node = new Node(val);
Node cursor = chain;
// 找到插入的位置前的的那一个节点
if (position >= 0 && position <= this.getLength()) {
for (int i = 1; i < position; i++) {
cursor = cursor.next;
}
}
// 插入
Node.next = cursor.next;
cursor.next = Node;
}
/**
* 删除指定位置的节点
* @param position
* @return
*/
public void delete(int position) {
if (chain == null)
return ;
if (position > 0 && position < this.getLength()) {
for (int i = 1; i < this.getLength()+1; i++)
//当前位置的上一个节点的Next 指向当前节点的Next 节点, 此时当前节点不存在于该链表中
if (i == position) {
this.getPosition(i-1).next = this.getPosition(i+1);
}
}
}
/**
* 链表逆置方法
* 将指向逆置, 更新原链表的下一个节点Next 为上一个节点,
* 原链表的第一个节点逆置后作为新链表的最后一个节点, 其Next 指向null 即可
*
* @return
*/
public Node reverse() {
Node pre = null;
Node cursor = chain;
// 当cursor.next==null时即cursor到尾部时再循环一次把cursor变成头指针。
while (cursor != null) {
Node cursorNext = cursor.next;
if (cursorNext == null) {
// 逆序后链表
chain = new Node();
chain.next = cursor;
}
if (pre != null && pre.getNext() == null && pre.getData() == null)
pre = null;
cursor.next = pre;
pre = cursor;
cursor = cursorNext;
}
return chain;
}
/**
* 链表下标从1开始,获取第position 个节点
*
* @param position
* @return
*/
public Node getPosition(int position) {
Node cursor = chain;
//节点插入的位置超出链表范围
if (position < 0 || position > this.getLength()) {
return null;
}
for (int i = 0; i < position; i++) {
cursor = cursor.next;
}
return cursor;
}
/**
* 获取倒数第position 个节点
*
* @param position
* @return
*/
public Node getBackPosition(int position) {
Node cursor = chain;
//节点插入的位置超出链表范围
if (position < 0 || position > this.getLength())
return null;
//找到倒数第position 个位置
for (int i = 0; i < this.getLength() - position + 1; i++)
cursor = cursor.next;
return cursor;
}
/**
* 链表存在环
* 追逐方法解决该问题
* 环模型:
* 存在环即链表中存在某个节点的Next 指向它前面的某个节点, 此时遍历链表是一个死循环
*
* @return
*/
public boolean judgeLoop() {
if (chain == null)
return false;
//快节点和慢节点从首位节点一起出发
Node fast = chain;
Node slow = chain;
//快节点每次走两步, 慢节点每次走一步, 如果是环, 则快节点会追上慢节点
while (fast.next != null && fast.next.next != null) {
fast = fast.next.next;
slow = slow.next;
if (fast == slow)
return true;
}
return false;
}
/**
* 计算环的长度
* @return
*/
public int getLoopLength() {
if (chain == null)
return 0;
Node fast = chain;
Node slow = chain;
// 标识是否相遇
boolean tag = false;
// 初始化环长
int length = 0;
// fast走两步, slow走一步, 快指针与慢指针第二次相遇时慢指针走的长度就是环的大小
while(fast.next != null && fast.next.next != null) {
fast = fast.next.next;
slow = slow.next;
// tag为true 是已经相遇过了,当fast == slow 此时是第二次相遇
if(fast == slow && tag == true) {
break;
}
// 初始化tag 为false, 第一次相遇标记tag 为true
if(fast == slow && tag == false) {
tag =true;
}
// 第一次相遇后开始计算环的长度
if(tag == true ) {
length++;
}
}
return length;
}
/**
* 环的入口
*
* 环后续单独拉出来写一个博客, 此处应该画个图更好理解
* 暂时先给个不错的链接: https://www.cnblogs.com/fankongkong/p/7007869.html
* @return
*/
public Node entryLoop(){
// 指向首节点
Node fast = chain;
Node slow = chain;
// 找环中相汇点, 快节点走两步慢节点走一步
while(slow.next != null && fast.next.next != null){
fast = fast.next.next;
slow = slow.next;
// 相遇, 此时慢节点走了x 个节点, 快节点走了2x 个节点, 设环有m 个节点, fast 多走了n 圈
// 有x + m*n = 2*x 即m*n = x, slow 走到环入口后就一直在环里, 所以相遇点距离环入口的长度和慢节点从首节点走到环入口的距离相等
if(fast == slow) {
break;
}
}
// fast 从相遇点一步一步走, slow 从首节点开始一步一步走, 走相同的距离相遇的点就是环的入口
slow = chain;
while(fast != slow){
fast = fast.next;
slow = slow.next;
}
return slow;
}
/**
* 清除链表元素
*/
public void clear() {
chain = new Node();
}
}
2.3 关于环的补充
关于环,在下面2-3 中测试类中模拟环,让尾节点指向首节点,实现一个简单的环结构。存在环结构的链表遍历会进入死循环。
测试类中在模拟环结构之前,链表结构如下:
模拟环结构之后,链表数据结构如下:
2.4 链表测试类TestChainTable.java
测试链表封装类的常用方法,以供参考。
/**
* @Description :链表测试类
* @Author: niaonao
* @Date: 2018/8/11 14:11
*/
public class TestChainTable {
//声明链表
private static ChainTable chainLink;
public static void main(String a[]) {
testChainMethod();
}
/**
* 测试链表方法
*/
private static void testChainMethod() {
System.out.println("\n1.1 初始化链表数据");
init();
if (chainLink == null || chainLink.getLength() < 1) {
return;
}
showChain();
System.out.println("\n2.1 链表第三个节点插入数据为Three 的节点");
chainLink.insert("Three", 3);
showChain();
System.out.println("\n3.1 获取链表中第5 个节点的数据: " + chainLink.getPosition(5).getData());
showChain();
System.out.println("\n4.1 获取链表中倒数第5 个节点的数据: " + chainLink.getBackPosition(5).getData());
showChain();
System.out.println("\n5.1 链表删除第2 个节点 ");
chainLink.delete(2);
showChain();
System.out.println("\n6.1 链表逆序");
chainLink.reverse();
showChain();
System.out.println("\n7.1 链表是否相交: " + chainLink.judgeLoop());
showChain();
System.out.println("\n7.2 模拟环模型, 使当前链表尾节点的Next 指向首节点");
Node firstNode = chainLink.getPosition(1);
Node lastNode = chainLink.getBackPosition(1);
lastNode.setNext(firstNode);
//出现环时, 链表遍历是个死循环
//showChain();
if (chainLink.judgeLoop()){
System.out.print("\n7.3 链表是否有环: " + Boolean.TRUE + "\n\t出现环时, 链表遍历是个死循环");
System.out.print("\n\t环的长度: " + chainLink.getLoopLength());
System.out.print("\n\t环的入口: " + chainLink.entryLoop()
+ "\n\t\t环入口节点的数据data: " + chainLink.entryLoop().getData()
+ "\n\t\t下一个节点对象node: " + chainLink.entryLoop().getNext());
}
else {
System.out.println("\n7.3 链表是否有环: " + Boolean.TRUE + "\n\t" + chainLink);
}
}
/**
* 初始化数据
*/
private static void init() {
chainLink = new ChainTable();
System.out.println("\t在链表首位添加A,链表尾部依此添加B、C、D、E");
chainLink.insertHead("A");
chainLink.insertTail("B");
chainLink.insertTail("C");
chainLink.insertTail("D");
chainLink.insertTail("E");
}
/**
* 链表数据显示
*/
private static void showChain() {
System.out.println("\t链表长度: " + chainLink.getLength());
System.out.print("\t链表数据: ");
for (int i = 0; i < chainLink.getLength(); i++) {
Node node = chainLink.getPosition(i + 1);
System.out.print("\t " + node.getData());
}
}
}
测试结果:
1.1 初始化链表数据
在链表首位添加A,链表尾部依此添加B、C、D、E
链表长度: 5
链表数据: A B C D E
2.1 链表第三个节点插入数据为Three 的节点
链表长度: 6
链表数据: A B Three C D E
3.1 获取链表中第5 个节点的数据: D
链表长度: 6
链表数据: A B Three C D E
4.1 获取链表中倒数第5 个节点的数据: B
链表长度: 6
链表数据: A B Three C D E
5.1 链表删除第2 个节点
链表长度: 5
链表数据: A Three C D E
6.1 链表逆序
链表长度: 5
链表数据: E D C Three A
7.1 链表是否相交: false
链表长度: 5
链表数据: E D C Three A
7.2 模拟环模型, 使当前链表尾节点的Next 指向首节点7.3 链表是否有环: true
出现环时, 链表遍历是个死循环
环的长度: 5
环的入口: Node@2503dbd3
环入口节点的数据data: E
下一个节点对象node: Node@4b67cf4d