链表( Linked List ),别名链式存储结构或单链表,是一种常见的基础数据结构,用于存储逻辑关系为 “一对一” 的数据。链表是线性表的一种,但是它并不像顺序表一样是连续存储在内存中的。链表的各个结点散布在各个内存区域,在每一个结点中都存放下一个结点的地址。
单链表在内存中是如下存储的:
其中:data 域存放的是本结点的数据,next 域存放的是下一个结点的地址。
通过上面的图,我们可以得到链表的几个特性:
链表是以结点的方式来存储的,是链式存储;
每一个结点包含 data 域、next 域。其中 next 域存放的是下一个结点的地址;
链表各个结点并不一定是连续(所谓连续指的是地址连续)存储的;
链表分为带头结点的链表和没有头结点的链表。
从上面的定义可以知道,链表中的每个结点中存放的是本结点的数据以及下一个结点的地址。所以链表中的结点可以定义如下:
public ListNode{
public int age; // 本结点的信息
public String name;
public ListNode next; // 下一个结点的地址
}
为了便于下面的演示,定义一个结点 HeroNode
如下,该结点可以理解为存储的是一个水浒英雄的信息:
class HeroNode {
private int no; // 本结点数据
private String name;
private String nickName;
public HeroNode next; // 指向下一个结点
public HeroNode(int no, String name, String nickName) {
this.no = no;
this.name = name;
this.nickName = nickName;
}
// getter and setter and toString ......
}
假设我现在有个需求,要求使用链表实现一个水浒英雄榜管理系统。这个系统的具体功能如下:
根据以上的 4 个需求,我们下面将分 4 步来逐一解决。
增加结点,对应的是需求 1,也就是直接在链表的末尾追加一个结点。
思路:
首先我们需要创建一个头结点,该结点的作用就是表示单链表的头,如果没有头结点,我们是无法知道链表的首个结点是谁、在哪;
单链表是单向的,所以我们需要从头结点开始遍历整个链表直到末尾,然后增加结点到链表的末尾;
需要注意的是,头结点是万万不能乱动的,所以我们最好将头结点复制到一个临时结点变量中,对临时变量进行遍历。
代码实现:
// 头结点中是不存储数据的,因此数据无所谓。只要存储下一个结点的地址即可
private static final HeroNode headNode = new HeroNode(0, "", "");
/**
* @Description 1. 插入结点到链表中(直接在末尾追加)
*/
private static void insertNode(HeroNode node) {
// 首先需要拷贝一份头结点
HeroNode tempNode = headNode;
// 插入结点到链表之中,需要首先遍历链表
while (true) {
// 判断有没有到达链表末尾
if (tempNode.next == null) {
// 如果当前结点的下一个结点为 null 时,说明当前结点是最后一个结点
tempNode.next = node;
node.next = null;
System.out.println("++++++++++++ 插入结点成功!");
break;
}
// 没有到达链表末尾,那就后移一个位置
tempNode = tempNode.next;
}
}
按顺序插入结点,对应的是需求 2,也就是根据编号从小到大的顺序将英雄插入到链表中。
思路:
代码实现:
/**
* @Description 2. 根据编号从小到大顺序插入结点
*/
public static void insertNodeByOrder(HeroNode node) {
// 头结点是代码的柱石,不能动,复制一份作为辅助变量
HeroNode tempNode = headNode;
// 遍历链表
while (true) {
if (tempNode.next == null) {
// 如果到了链表末尾,就直接插入
tempNode.next = node;
node.next = null;
break;
}
if (tempNode.next.getNo() > node.getNo()) {
// 如果当前结点的下一个结点的编号大于要插入的结点,就插入
// 先让要插入的结点指向下一个结点
node.next = tempNode.next;
// 然后再让当前结点指向要插入的结点
tempNode.next = node;
break;
} else if (tempNode.next.getNo() == node.getNo()) {
// 说明该结点已存在
System.out.println("!!!!!!!!!! 该结点已存在");
break;
}
// 上述条件都不满足,就继续往后迭代
tempNode = tempNode.next;
}
}
删除结点,就是根据编号去查找结点,然后把结点删除掉。
思路:
代码实现:
/**
* @Description 3. 从链表中删除结点
*/
private static void deleteNode(int no) {
// 首先获取到头结点
HeroNode tempNode = headNode;
// 遍历结点,找到要删除的结点的前一个结点
while (true) {
// 判断当前结点是不是最后一个结点,如果到了末尾还没有找到目标结点,报错警告
if (tempNode.next == null) {
System.out.println("!!!!!!!!!! 目标结点不存在!");
break;
}
if (tempNode.next.getNo() == no) {
// 说明找到了要删除的结点
// 所谓删除结点,其实就是把要删除的结点的前一个结点的 next 指向要删除的结点的后一个结点
tempNode.next = tempNode.next.next;
System.out.println("------------ 更新结点成功!");
break;
}
// 没有找到目标结点,就继续往后移一个位置
tempNode = tempNode.next;
}
}
更新结点,对应的是需求 4,也就是根据编号去查找结点,然后更新结点的信息(不包括编号,因为编号是唯一标识)。
思路:
代码:
/**
* @Description 4. 更新结点信息
*/
private static void updateNode(HeroNode node) {
// 首先获取头结点
HeroNode tempNode = headNode;
// 遍历结点,找到要修改的结点的前一个结点
while (true) {
// 判断当前结点是不是最后一个结点,如果到了末尾还没有找到目标结点,报错警告
if (tempNode.next == null) {
System.out.println("!!!!!!!!!! 目标结点不存在!");
break;
}
if (tempNode.next.getNo() == node.getNo()) {
// 更新结点信息
tempNode.next.setName(node.getName());
tempNode.next.setNickName(node.getNickName());
System.out.println("*********** 更新结点成功!");
break;
}
tempNode = tempNode.next;
}
}
需求:
实现一种算法,找出单向链表中倒数第 k 个结点,返回该结点的值。
示例:
输入: 1->2->3->4->5 和 k = 2
输出: 4
思路:
代码实现:
/**
* @Description 根据链表的头结点和 k 到链表中查找倒数第 k 个节点
*/
public static int getItem(HeroNode head, int k){
// 1. 首先获取链表总长度
int size = getSize(head);
// 2. 链表总长度减去 k 个节点,得到要遍历的节点个数
int length = size - k;
// 3. 获取头结点
HeroNode temp = head;
// 4. 遍历到倒数第 k 个节点
for (int i=0; i<length+1; i++){
temp = temp.next;
}
return temp.getNo();
}
/**
* @Description 获取链表中的结点的总个数
*/
public static int getSize(HeroNode head){
// 1. 获取头结点
HeroNode temp = head;
int length = 0;
// 2. 遍历链表,获取节点个数
while (true){
if (temp.next == null){
// 如果到了末尾
break;
}
length++;
temp = temp.next;
}
return length;
}
需求:
反转一个单链表。
示例:
输入: 1->2->3->4->5->NULL
输出: 5->4->3->2->1->NULL
思路:
解决这个问题的核心就是头插法。
代码实现:
/**
* @Description 反转链表
* @Param [head] 头结点
*/
public static void reverseList(HeroNode head){
// 1. 判断链表是否为空或者只有一个结点
if (head.next == null || head.next.next == null){
return;
}
// 2. 定义一个辅助的指针变量,帮助我们遍历原来的链表
HeroNode cur = head.next;
HeroNode next = null; // 指向当前结点[cur] 的下一个结点
HeroNode reverHead = new HeroNode(0, "", ""); // 用于临时存放反转过程中的链表
while (cur != null){
next = cur.next; // 先把当前结点的下一个结点保存下来
cur.next = reverHead.next; // 接着让当前结点指向临时反转链表的第一个结点
reverHead.next = cur; // 让当前结点作为临时反转链表的第一结点
cur = next; // 移动到原来链表的下一个结点,继续遍历
}
head.next = reverHead.next; // 让原头结点指向反转后的链表
}
需求:
逆序打印一个单链表。
示例:
输入: 1->2->3->4->5->NULL
输出: 5->4->3->2->1
思路:
这个问题的解决有多种方案,下面将对各种方案分别进行介绍。
方案一:使用栈。根据栈的先进后出特点,可以先正向遍历将链表中的结点入栈,然后让结点出栈,在出栈的时候打印即可。
方案二:使用递归。首先判断当前结点是否为最后一个结点,如果不是则递归调用,然后在判断语句之外打印每个结点。
方案三:直接反转单链表。这个方案就是 3.2 的方案,先将链表反转,然后遍历打印。但是这样会破坏原链表的结构,不推荐使用。
代码实现:
方案一:使用栈
/**
* @Description 方案一:使用栈来逆序打印结点
* @Param [head] 头结点
*/
public static void printReverse_2(HeroNode head){
Stack<HeroNode> nodeStack = new Stack<>();
// 去除头结点
HeroNode cur = head.next;
// 结点入栈
while (cur != null){
nodeStack.push(cur);
cur = cur.next;
}
// 结点出栈并打印
while (nodeStack.size() > 0){
System.out.println(nodeStack.pop());
}
}
方案二:使用递归
/**
* @Description 方案二:使用递归打印结点
* @Param [node] 链表的第一个结点,即 head.next
*/
public static void printReverse_3(HeroNode node){
// 这里一定要先递归调用再打印
if (node.next != null){
printReverse_3(node.next);
}
System.out.println(node);
}
需求:
合并两个有序链表,并且要求合并后的链表依然是一个有序链表。
示例:
输入:1->4->7->NULL
2->3->5->NULL
输出:1->2->3->4->5->7->NULL
思路:
这个问题的核心思想其实就是上面的 2.2 按顺序插入结点到链表中。
代码:
/**
* @Description 将 head_1 指向的链表按照结点编号从小到大顺序合并到 head_2 指向的链表中
* @Param [head_1, head_2]
* @return 合并后的有序链表
*/
public static HeroNode collectList(HeroNode head_1, HeroNode head_2){
HeroNode cur = head_1.next;
HeroNode next;
// 遍历第一个链表,将每个结点插入到第二个链表中
while(cur != null){
next = cur.next;
insertNode(cur, head_2);
cur = next;
}
return head_2;
}
/**
* @Description 按照编号从小到大的顺序插入一个结点到 head 指向的链表中
* @Param [node, head]
* @return void
*/
public static void insertNode(HeroNode node, HeroNode head){
HeroNode temp = head;
while (true){
if (temp.next == null){
// 说明遍历到了链表的末尾,直接追加即可
temp.next = node;
// 需要注意的是,由于插入的结点可能还记录着其它结点,所以必须要把这个结点记录的地址清空
// 这样才能顺利记录下一个结点
node.next = null;
break;
}
if (temp.next.getNo() > node.getNo()){
// 如果该结点小于要插入的链表的结点,那么就把这个结点插入到第一个小于的结点的前一个
node.next = temp.next;
temp.next = node;
break;
}
temp = temp.next;
}
}