一、概念
单链表是一种链式存取的数据结构,用一组地址任意的存储单元存放线性表中的数据元素。链表中的数据是以结点来表示的,每个结点的构成:元素(数据元素的映象) + 指针(指示后继元素存储位置),元素就是存储数据的存储单元,指针就是连接每个结点的地址数据。
节点结构:
- data域–存放结点值的数据域;
- next域–存放结点的直接后继的地址(位置)的指针域(链域)。
头指针head和终端结点:
- 单链表中每个结点的存储地址是存放在其前趋结点next域中,而开始结点无前趋,故应设头指针head指向开始结点。链表由头指针唯一确定,单链表可以用头指针的名字来命名;
- 终端结点无后继,故终端结点的指针域为空,即NULL。
二、单链表的相关操作
1. 单链表的插入
直接把节点添加到链表的尾部。
思路:
- 先创建一个head头结点(单链表不存在的情况下),表示单链表的头;
- 通过定义一个辅助变量 temp 指向 head (因为head是链表的头结点,不能移动)来遍历链表,找到链表的最后一个节点;
- 链表的最后一个节点的next域指向新节点。
图解:
2. 链表按顺序插入
按照HeroNode的no递增插入节点。
思路:
- 根据比较节点的no属性的大小来找到所要插入节点的位置;
- 设置一个标记位 flag 标记插入节点的编号是否已存在(若编号已存在则不添加),初始值为false;
- temp.next.no > 新节点.no:找到插入节点的位置,新节点就插入在temp的后面;
- emp.next.no == heroNode.no:所要插入节点的编号已存在,所以令 flag = true;
- 若到最后 flag 仍然为 false:新节点.next = temp.next ,temp.next = 新节点;
- flag = true:需要插入节点和已存在的节点的编号重复,故不插入,并给出提示。
图解:
3. 修改链表节点信息
思路:
- 通过传入节点的 no 遍历查询链表中所要修改的节点的信息;
- 设置一个标记位 flag 标记是否找到所要修改的节点,初始值为false;
- 根据 判断传入节点.no == temp.no 来找到所要修改的节点;
- 若找到所要修改的节点则把 flag 设置为 true;
- flag == true:把传入节点的信息赋值给 temp 节点(此时temp代表要修改的节点),no属性不赋值;
- flag == false:找不到要修改的节点,给出提示信息。
4. 删除指定节点
根据传入的 no 遍历查询链表,删除编号为 no 的节点。
思路:
- 通过传入的 no 遍历链表;
- 设置一个标记位 flag 标记是否找到所要删除节点的上一个节点,初始值为false;
- temp.next.no == no:表示找到了所要删除节点的上一个节点temp,设置 flag = true;
- 根据flag表示是否要删除节点;
- flag = true:temp.next = temp.next.next;
- flag = false:未找到所要删除的节点。
图解:
5. 遍历链表
6. 上述操作代码实现
定义一个单链表:
class HeroNode {
public int no;
public String name;
public String nickName;
public HeroNode next;
public HeroNode(int no, String name, String nickName) {
this.no = no;
this.name = name;
this.nickName = nickName;
}
@Override
public String toString() {
return "HeroNode{" +
"no=" + no +
", name='" + name + '\'' +
", nickName='" + nickName + '\'' +
'}';
}
}
单链表管理类:
/**
* 定义SingleLinkedList,管理HeroNode
*/
class SingleLinkedList {
//先初始化一个头结点,不存放具体的数据
private HeroNode head = new HeroNode(0,null,null);
public HeroNode getHead() {
return head;
}
/**
* 向单链表中添加节点(添加到最后).
* @param heroNode
*/
public void add(HeroNode heroNode) {
//因为头结点不能动,所以我们需要一个辅助变量指向头结点
HeroNode temp = head;
//变量链表,找到最后一个节点
while (true) {
//指向了链表的最后一个节点
if (temp.next == null) {
break;
}
//指针后移
temp = temp.next;
}
//把新节点添加到链表最后一个节点的next
temp.next = heroNode;
}
/**
* 根据 HeroNode的no顺序进行节点的添加(递增)
* @param heroNode
*/
public void addByOrder (HeroNode heroNode) {
//因为头结点不能动,所以我们需要一个辅助变量指向头结点
HeroNode temp = head;
//新增一个标志位,标志节点是否可以添加
boolean flag = false;
while (true) {
//遍历到了最后一个节点
if (temp.next == null) {
break;
}
//位置找到,就在temp的后面插入
if (temp.next.no > heroNode.no) {
break;
} else if (temp.next.no == heroNode.no) {
flag = true; //说明编号存在
break;
}
temp =temp.next;
}
//判断flag的值决定是否添加节点
if (flag) {
System.out.printf("准备插入的节点 %d 已存在",heroNode.no);
System.out.println();
} else {
heroNode.next = temp.next;
temp.next = heroNode;
}
}
/**
* 修改链表的节点信息(根据no查找所要修改的节点)
* @param heroNode
*/
public void update(HeroNode heroNode) {
if (head.next == null) {
System.out.println("链表为空,不能修改");
return;
}
//因为头结点不能动,所以我们需要一个辅助变量指向头结点的下一个节点
HeroNode temp = head.next;
//标志是否找到所要修改的节点
boolean flag = false;
while (true) {
//遍历到了链表最后
if (temp == null) {
break;
}
//找到所要修改的节点
if (temp.no == heroNode.no) {
flag = true;
break;
}
temp = temp.next;
}
//根据flag判断是否修改节点
if (flag) {
temp.name = heroNode.name;
temp.nickName = heroNode.nickName;
} else {
System.out.printf("未找到编号为 %d 的节点\n", heroNode.no);
}
}
/**
* 删除指定的链表节点
* @param no
*/
public void delete(int no) {
if (head.next == null) {
System.out.println("链表为空,不能删除");
return;
}
//因为头结点不能动,所以我们需要一个辅助变量指向头结点
HeroNode temp = head;
//标志是否找到所要删除节点的上一个节点
boolean flag = false;
while (true) {
//遍历到了链表最后
if (temp.next == null) {
break;
}
//找到所要删除节点的上一个节点
if (temp.next.no == no) {
flag = true;//表示找到了所要删除节点的上一个节点temp
break;
}
temp = temp.next;
}
//根据flag表示是否要删除节点 到了这里temp是所要删除节点的上一个节点
if (flag) {
temp.next = temp.next.next;
} else {
System.out.printf("未找到编号为 %d 的节点\n", no);
}
}
/**
* 遍历链表.
*/
public void list() {
//链表为空
if (head.next == null) {
System.out.println("链表为空!");
return;
}
//因为头结点不能动,所以我们需要一个辅助变量指向头结点的下一个节点
HeroNode temp = head.next;
while (true) {
//判断是否遍历到了链表最后
//这里不能是temp.next == null
if (temp == null) {
break;
}
//输出节点信息
System.out.println(temp);
temp = temp.next;
}
}
}
三、单链表经典面试题
1. 获取链表的节点个数。
即链表长度,不包括头结点。
/**
* 求一个单链表的节点个数(即链表长度,不包括头结点)
* @param singleLinkedList
* @return
*/
public static int getSingleLinkedListLength(SingleLinkedList singleLinkedList) {
//因为头结点不能动,所以我们需要一个辅助变量指向头结点的下一个节点
HeroNode temp = singleLinkedList.getHead().next;
int length = 0;
while (temp !=null) {
length ++;
temp = temp.next;
}
return length;
}
2. 返回单链表中的倒数第 index 个结点
思路:
- 编写一个方法,传入一个单链表,同时传入一个index,index 表示的是此单链表中倒数第index个节点;
- 先把链表从头到尾遍历,得到链表的总的长度(调取上面的getSingleLinkedListLength方法即可);
- 得到size 后,我们从链表的第一个开始遍历 (size-index)个,就可以得到;
- 如果找到了,则返回该节点,否则返回null。
代码实现:
/**
*查找单链表中的 倒数 第 index 个结点
* @param singleLinkedList
* @param index
* @return
*/
public static HeroNode findReverseIndexNode(SingleLinkedList singleLinkedList,int index) {
//因为头结点不能动,所以我们需要一个辅助变量指向头结点的下一个节点
HeroNode temp = singleLinkedList.getHead().next;
//链表为空
if (temp == null) {
return null;
}
//获取链表长度
int size = getSingleLinkedListLength(singleLinkedList);
//遍历到size-index 位置,就是我们倒数的第 index 个节点
for (int i = 0;i < size -index; i++) {
temp = temp.next;
}
return temp;
}
3. 将单链表反转
思路:
- 创建一个新的单链表,遍历原旧链表,依次把旧链表的每一个节点通过头插法的方式添加在新链表里,然后把原链表的头结点指向新链表头结点的下一个节点即可。
代码实现:
/**
* 将单链表反转(头插法)
* @param singleLinkedList
*/
public static void reverseSingleLinkedList(SingleLinkedList singleLinkedList) {
//因为头结点不能动,所以我们需要一个辅助变量指向头结点的下一个节点
HeroNode temp = singleLinkedList.getHead().next;
//链表为空,或者链表只有一个节点
if (temp == null || temp.next == null) {
return;
}
//定义一个新的头结点
HeroNode reverseHead = new HeroNode(0,"","");
//定义一个辅助变量用于存储temp.next
HeroNode next = null;
//遍历原来的链表,每遍历一个节点,就将其取出,并放在新的链表reverseHead 的最前端(头插法)
while (temp != null) {
next = temp.next;
temp.next = reverseHead.next; //当前节点的下一个节点为reverseHead的下一个节点,即头插
reverseHead.next = temp; //reverseHead的下一个节点就是当前节点temp
temp = next; //temp下移
}
//将原链表的头结点指向新链表头结点的下一个节点,完成原链表的反转
singleLinkedList.getHead().next = reverseHead.next;
}
4. 逆序打印单链表
思路:
- 不破坏链表结构的前提下,通过栈实现即可。
代码实现:
/**
* 逆序打印单链表
* @param singleLinkedList
*/
public static void reversePrintSingleLinkedList(SingleLinkedList singleLinkedList) {
//因为头结点不能动,所以我们需要一个辅助变量指向头结点的下一个节点
HeroNode temp = singleLinkedList.getHead().next;
if(temp == null) {
return;//空链表,不能打印
}
//创建要给一个栈,将各个节点压入栈
Stack<HeroNode> stack = new Stack<HeroNode>();
//将链表的所有节点压入栈
while(temp != null) {
stack.push(temp);
temp = temp.next;
}
//将栈中的节点进行打印,pop 出栈
while (stack.size() > 0) {
System.out.println(stack.pop());
}
}
5. 合并两个有序的单链表,要求合并后的链表仍然是有序的
思路:
- 定义一个函数,传入两个有序的链表的管理对象SingleLinkedList,获取到两个有序链表的头结点;
- 函数形参中获取的两个头结点,把其中一个作为新链表的头结点;
- 定义temp1、temp2、newTemp,分别指向形参1获取的头结点的下一个节点、形参2获取的头结点的下一个节点、新链表的下一个节点(即辅助变量,因为头结点不能移动);
- 当指针temp1和指针temp2均未到达链表尾时,比较temp1和temp2的no值,从temp1或者temp2这两者中选择no值较小的节点插入到newTemp的后面;
- 如果temp1已经到达链表1的结尾,依次将链表1剩余元素插入到newTemp的最后;
- 如果temp2已经到达链表2的结尾,依次将链表1剩余元素插入到newTemp的最后;
代码实现:
/**
* 合并两个有序的单链表,要求合并后的链表仍然是有序的.
* @param list1
* @param list2
* @return
*/
public static SingleLinkedList connOrderListByOrder(SingleLinkedList list1,SingleLinkedList list2) {
//以list1管理的链表的头结点作为新链表的头结点
SingleLinkedList newList = list1;
//头结点不能动,所以定义一个辅助变量指向新链表的头结点
HeroNode newTemp = newList.getHead();
//头结点不能动,所以定义两辅助变量指向头结点的下一个节点
HeroNode temp1 = list1.getHead().next;
HeroNode temp2 = list2.getHead().next;
//list1和list2管理的链表均未到达表尾,依次摘取两表中值较小的节点插入到newHead后面
while (temp1!=null && temp2!=null) {
if (temp1.no <= temp2.no) { //摘取list1所管理的链表的结点
newTemp.next = temp1;
newTemp = newTemp.next; //newTemp 下移
temp1 = temp1.next;
} else { //摘取list2所管理的链表的结点
newTemp.next = temp2;
newTemp = newTemp.next;
temp2 = temp2.next;
}
}
//若list1管理的链表已经遍历到结尾,则直接把list2所管理的链表直接接到新链表的后面
if (temp1 == null) {
newTemp.next = temp2;
}
//同理
if (temp2 == null) {
newTemp.next = temp1;
}
//返回新的链表管理
return newList;
}
}