链表介绍
链表是有序的列表,但是它在内存中是存储结构如左下,逻辑结构如右下:

- 链表是以节点的方式来存储,是链式存储;
- 每个节点包含 data 域, next 域(指向下一个节点);
- 链表的各个节点不一定是连续存储;
- 链表分带头节点的链表和没有头节点的链表,根据实际的需求来确定。
单链表的应用实例
使用带 head 头的单向链表实现 –水浒英雄排行榜管理完成对英雄人物的增删改查操作。 这里分两种实现,一种按排名插入,一种直接插入到链表尾部;
前提
- head 头节点不保存任何数据,只是为了找到链表的第一个元素;
- 在遍历时要保持 head 节点不改变,目的是不丢失链表头。
遍历时创建一个临时变量代替 head 节点遍历整个链表。
直接尾部插入:
- 先创建一个 head 头节点,表示单链表的头,不可移动;
- 找到链表中最后一个元素;(需要遍历,使用临时变量代替 head 节点遍历)
- 将最后一个元素的 next 指向要插入的元素;
- 遍历时:为了保证 head 头节点保持不动,需要一个辅助变量来循环遍历链表;
实现:
水浒英雄节点类
```java package com.zsy.datastructure.linkedlist;
/**
@author zhangshuaiyin */ public class HeroNode {
int no; String name; String nickName; HeroNode next;
public HeroNode(int no, String name, String nickName) {
this.no = no;this.name = name;this.nickName = nickName;
}
@Override public String toString() {
if (next == null) {return "HeroNode{" +"no=" + no +", name='" + name + '\'' +", nickName='" + nickName + '\'' +", next=" + next +'}';} else {return "HeroNode{" +"no=" + no +", name='" + name + '\'' +", nickName='" + nickName + '\'' +'}';}
单链表实现(尾部新增+遍历)
```java package com.zsy.datastructure.linkedlist;
/**
@author zhangshuaiyin */ public class SingleLinkedList {
private final HeroNode head = new HeroNode(0, “”, “”);
public void add(HeroNode node) {
HeroNode temp = head;while (temp.next != null) {temp = temp.next;}temp.next = node;
}
public void list() {
if (head.next == null) {throw new RuntimeException("当前链表为空");}HeroNode temp = head.next;while (temp != null) {System.out.println(temp);temp = temp.next;}
测试
```java package com.zsy.datastructure.linkedlist;
/**
@author zhangshuaiyin */ 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 linkedList = new SingleLinkedList();linkedList.add(hero1);linkedList.add(hero4);linkedList.add(hero2);linkedList.add(hero3);linkedList.list();
2. 根据编号大小插入
分析:
- 创建 temp 临时节点代替 head 遍历链表,找到插入位置 temp.next;
- 遍历时要判断编号不能重复,如果重复,提示错误信息;
插入步骤:new.next = temp.next; temp.next = new;
实现:按编号大小插入方法
/*** 按照编号由小到大的顺序插入节点,编号不可重复** @param node 插入节点*/public void addByOrder(HeroNode node) {HeroNode temp = head;while (temp.next != null) {if (temp.next.no == node.no) {System.out.printf("编号 %d 已存在,插入失败\n", node.no);return;} else if (temp.next.no > node.no) {// 此时说明新节点应该插入 temp 的下一个位置break;}temp = temp.next;}node.next = temp.next;temp.next = node;}
3. 根据编号修改
分析:
创建 temp 临时节点代替 head 遍历链表,找到要修改的节点 temp;
修改操作:temp.name = node.name; temp.nickName = node.nickName;
实现:
/*** 根据新节点信息修改链表中相同编号的节点** @param node 新节点*/public void update(HeroNode node) {HeroNode temp = head;while (temp.next != null) {temp = temp.next;if (temp.no == node.no) {temp.name = node.name;temp.nickName = node.nickName;return;}}System.out.printf("没有找到编号是 %d 的节点,不能修改\n", node.no);}
4. 根据编号删除
分析:
创建 temp 临时节点代替 head 遍历链表,找到要删除的节点 temp.next(temp 是要删除节点的前一个节点);
删除操作:temp.next = temp.next.next;
实现:
/*** @param no 根据编号 no 删除链表中对应的节点*/public void delete(int no) {HeroNode temp = head;while (temp.next != null) {if (temp.next.no == no) {temp.next = temp.next.next;return;}temp = temp.next;}System.out.printf("没有找到编号是 %d 的节点,不能修改\n", no);}
面试题实现
求单链表中有效节点的个数
/*** 获取单链表有效节点个数** @return 有效节点数*/public int getLength() {HeroNode temp = head;int length = 0;while (temp.next != null) {length++;temp = temp.next;}return length;}
查找单链表中的倒数第 k 个结点 【新浪面试题】
分析:
首先遍历获取链表有效节点个数 size;
- 然后再次遍历 size-k+1 次,即找到倒数第 k 个节点(+1是因为下标从0开始)
- 四个数,倒数第一就是整数第四(4-1+1);倒数第二就是整数第三(4-2+1)
/*** 查找单链表中的倒数第 k 个结点** @param k 倒数第 k 个* @return 找得到返回节点对象,找不到返回 null*/public HeroNode getLastIndexNode(int k) {HeroNode temp = head;int size = getLength();// 空链表返回空if (temp.next == null || k > size || k <= 0) {return null;}// 比如 size = 3; k = 2; i =1 找第二个(下标是1)temp = temp.next;for (int i = 0; i < size - k; i++) {temp = temp.next;}return temp;}
单链表的反转【腾讯面试题】
分析:
- 定义 reverseHead 作为反转链表的头;
- 遍历原来的链表,从头依次取出元素,然后添加到反转链表的头节点后面(添加到最前面)
操作: head.next = reverseHead.next
/*** 单链表的反转* 1. 创建一个新的头节点用于保存反转后的链表* 2. 遍历原来的链表,依次取出元素放置新头节点的后面*/public void reverse() {if (head.next == null || head.next.next == null) {return;}// 反转后链表头节点HeroNode revereHead = new HeroNode(0, "", "");// 遍历用辅助节点HeroNode temp = head.next;// 取出节点的下一个节点,保存遍历指针HeroNode next = null;while (temp != null) {// 标记下一个元素,此时temp节点就是取出的节点next = temp.next;// 将反转链表节点放到取出的第一个节点后面temp.next = revereHead.next;// 将取出的节点(包含反转链表的节点)放到反转链表第一个位置revereHead.next = temp;// 辅助节点后移temp = next;}head.next = revereHead.next;}
从尾到头打印单链表【百度面试题】
分析:
- 可以先将单链表反转再打印链表,但是会破坏单链表的结构,不推荐;
- 使用栈先进后出的特性实现逆序打印单链表;
/*** 逆序打印链表信息(栈)*/public void reverseList() {if (head.next == null) {throw new RuntimeException("当前链表为空");}HeroNode temp = head.next;Stack<HeroNode> stack = new Stack<>();while (temp != null) {stack.push(temp);temp = temp.next;}while (stack.size() > 0) {System.out.println(stack.pop());}}
合并两个有序的单链表,合并之后的链表依然有序
分析:
- 创建临时头节点保存合并后的节点信息;
- 同时遍历两个单链表,根据条件比较大小;
- 较小的节点放到临时头节点的后面即可;
其中一个单链表遍历结束,另一个单链表剩余的数据直接添加到最后;
/*** 合并两个有序的单链表,合并之后的链表依然有序** @param linkedList 要合并的单链表*/public void mergeWithOrder(SingleLinkedList linkedList) {HeroNode otherHead = linkedList.head;if (head.next == null && otherHead.next == null) {throw new RuntimeException("要合并的两个链表为空~");} else if (head.next == null) {head.next = otherHead.next;} else if (otherHead.next == null) {return;}// 保存合并后链表的头节点HeroNode node = new HeroNode(0, "", "");HeroNode temp0 = node;HeroNode temp1 = head.next;HeroNode temp2 = otherHead.next;// 同时遍历两个有序单链表while (temp1 != null && temp2 != null) {// 遍历两个链表,比较编号大小,比较小的放到合并链表的最后if (temp1.no < temp2.no) {temp0.next = temp1;temp1 = temp1.next;} else {temp0.next = temp2;temp2 = temp2.next;}// 合并后链表的辅助节点后移temp0 = temp0.next;}// 当有链表遍历结束,直接将另一个链表剩余的数据放到最后if (temp1 == null) {temp0.next = temp2;}if (temp2 == null) {temp0.next = temp1;}// 返回合并后链表的头节点head.next = node.next;}
