链表
链表 (Linked List) 介绍
链表是有序的列表,但是它在内存中是存储如下
- 小结上图:
- 链表是以节点的方式来存储,是链式存储
- 每个节点包含 data 域, next 域:指向下一个节点.
- 如图:发现链表的各个节点不一定是连续存储.
- 链表分带头节点的链表和没有头节点的链表,根据实际的需求来确定
- 单链表(带头结点) 逻辑结构示意图如下
单链表的应用实例
使用带head头的单向链表实现 –水浒英雄排行榜管理 ,完成对英雄人物的增删改查操作
第一种方法在添加英雄时,直接添加到链表的尾部
思路分析示意图
第二种方式在添加英雄时,根据排名将英雄插入到指定位置 (如果有这个排名,则添加失败,并给出提示)
思路分析示意图
修改节点功能
- 思路
- 先找到该节点,通过遍历,
temp.name = newHeroNode.name ; temp.nickname=newHeroNode.nickname
删除节点
- 思路分析的示意图:
代码
package com.www.linked;
import java.util.Stack;
/**
* 单向链表
* <p>
*
* @author Www
* <p>
* 邮箱: 483223**@qq.com
* <p>
* 创建时间: 2022/7/29 11:35 星期五
* <p>
*/
public class LinkedListTest {
/**
* 方法入口
*
* @param args 参数
*/
public static void main(String[] args) {
HeroNode hero1 = new HeroNode(1, "宋江", "及时雨");
HeroNode hero2 = new HeroNode(2, "卢俊义", "玉麒麟");
HeroNode hero3 = new HeroNode(3, "吴用", "智多星");
HeroNode hero5 = new HeroNode(5, "吴用", "智多星");
HeroNode hero4 = new HeroNode(4, "林冲", "豹子头");
// 创建要给数据的链表
SingleLinkedList singleLinkedList = new SingleLinkedList();
// 加入数据
singleLinkedList.addByOrder(hero5);
singleLinkedList.addByOrder(hero1);
singleLinkedList.addByOrder(hero3);
singleLinkedList.addByOrder(hero2);
singleLinkedList.addByOrder(hero3);
singleLinkedList.addByOrder(hero4);
// 获取头节点
HeroNode head = singleLinkedList.getHead();
System.out.println(head);
// 展示链表数据
singleLinkedList.list();
// 修改
boolean update = singleLinkedList.update(new HeroNode(5, "有用", "至多某"));
System.out.println("修改节点 " + update);
System.out.println("\n=============== 修改后的链表 =================\n");
// 展示链表数据
singleLinkedList.list();
// 删除
/* boolean delete = singleLinkedList.delete(5);
boolean delete3 = singleLinkedList.delete(3);
boolean delete2 = singleLinkedList.delete(2);
boolean delete4 = singleLinkedList.delete(4);
boolean delete1 = singleLinkedList.delete(1);
System.out.println("删除节点 = " + delete);
System.out.println("删除节点 = " + delete3);
System.out.println("删除节点 = " + delete2);
System.out.println("删除节点 = " + delete4);
System.out.println("删除节点 = " + delete1);*/
System.out.println("\n=============== 删除后的链表 =================\n");
// 展示链表数据
singleLinkedList.list();
// 获取具体的节点
System.out.println("singleLinkedList.getHeroNodeByNo(5) = " + singleLinkedList.getHeroNodeByNo(5));
// 获取有效节点的个数(不包括头节点)
Integer effectLength = singleLinkedList.getEffectLength(singleLinkedList.getHead());
System.out.println("effectLength = " + effectLength);
System.out.println("singleLinkedList.findLastIndexNode(singleLinkedList.getHead(),2) = " + singleLinkedList.findLastIndexNode(singleLinkedList.getHead(), effectLength));
// 反转
singleLinkedList.reverseList(singleLinkedList.getHead());
System.out.println("反转后的链表 ");
// 展示链表数据
singleLinkedList.list();
// 反转链表 ,不改变原链表
System.out.println(" 反转链表 ,不改变原链表 ");
singleLinkedList.reversePrint(singleLinkedList.getHead());
}
}
/**
* 定义 SingleLinkedList 管理 英雄
*/
class SingleLinkedList {
/**
* 先初始化一个头节点, 头节点不动 , 不存放具体的数据
*/
private final HeroNode head = new HeroNode(0, "", "");
/**
* 返回头节点
*
* @return 头节点
*/
public HeroNode getHead() {
return head;
}
/**
* 添加节点到单向链表
* <p>
* 不考虑编号
* <p>
* 找到当前链表的最后一个节点
* <p>
* 将最后一个节点的 next 指向新的节点
*
* @param heroNode 待添加的节点
*/
public void add(HeroNode heroNode) {
// 头节点不能动 ,需要一个辅助遍历 tmp
HeroNode temp = getHead();
// 判断要添加的元素是否已经存在
boolean flag = false;
// 遍历 链表 ,找到最后一个节点
while (temp.getNext() != null) {
if (temp.getNext().getNo() == heroNode.getNo()) {
flag = true;
break;
}
// 没有找到 将 temp 后移
temp = temp.getNext();
}
if (flag) {
System.out.printf("准备插入的英雄的编号 %d 已经存在了, 不能加入\n", heroNode.getNo());
} else {
temp.setNext(heroNode);
}
}
/**
* 添加并排序
*
* @param heroNode 待添加的节点
*/
public void addByOrder(HeroNode heroNode) {
// 头节点不能动 ,需要一个辅助遍历 tmp
// 因为是单链表, 且我们找的是位于添加位置的前一个节点,否则插入不了
HeroNode temp = getHead();
// flag 标志添加的编号是否已存在,默认为 false
boolean flag = false;
while (temp.getNext() != null) {
if (temp.getNext().getNo() > heroNode.getNo()) {
break;
} else if (temp.getNext().getNo() == heroNode.getNo()) {
// 说明希望添加的 heroNode 的编号已经存在
flag = true;
break;
}
// 后移, 遍历当前的链表
temp = temp.getNext();
}
if (flag) {
System.out.printf("准备插入的英雄的编号 %d 已经存在了, 不能加入\n", heroNode.getNo());
} else {
// 插入到链表中 , temp的后面
heroNode.setNext(temp.getNext());
temp.setNext(heroNode);
}
}
/**
* 遍历链表
*/
public void list() {
// 判断链表是否为空
if (head.getNext() == null) {
System.out.println("链表为空!!!");
return;
}
HeroNode temp = head.getNext();
while (temp != null) {
// 输出节点信息
System.out.println(temp);
// 将 temp 节点信息后移
temp = temp.getNext();
}
}
/**
* 修改
*
* @param heroNode
*/
public boolean update(HeroNode heroNode) {
// 判断链表是否为空
if (head.getNext() == null) {
System.out.println("链表为空 !!!");
}
// 找到修改的节点 ,根据 no 编号
// 定义一个辅助变量
HeroNode temp = head.getNext();
boolean flag = false;
while (temp != null) {
if (temp.getNo() == heroNode.getNo()) {
flag = true;
break;
}
// temp 向后移 ,实现遍历
temp = temp.getNext();
}
if (flag) {
// 找到了要修改的数据
temp.setName(heroNode.getName());
temp.setNickName(heroNode.getNickName());
return true;
} else {
// 没找到
System.out.printf("没有找到 编号 %d 的节点,不能修改\n", heroNode.getNo());
return false;
}
}
/**
* 根据 No 删除节点
*
* @param no 待删除节点的 No
* @return true 删除成功 ; false 删除失败
*/
public boolean delete(Integer no) {
HeroNode temp = head;
boolean flag = false;
while (temp.getNext() != null) {
if (temp.getNext().getNo() == no) {
// 找到了要删除节点 的前一个节点 temp
flag = true;
break;
}
temp = temp.getNext();
}
if (flag) {
// 删除
temp.setNext(temp.getNext().getNext());
return true;
} else {
System.out.printf("要删除的 %d 节点不存在\n", no);
return false;
}
}
/**
* 根据 No 获取英雄的详细信息
*
* @param no No
* @return HeroNode 对象
*/
public HeroNode getHeroNodeByNo(Integer no) {
HeroNode temp = head.getNext();
if (temp == null) {
System.out.println("链表为空 !!!");
}
boolean flag = false;
while (temp != null) {
if (temp.getNo() == no) {
flag = true;
break;
}
temp = temp.getNext();
}
if (flag) {
// 找到了
return new HeroNode(no, temp.getName(), temp.getNickName());
} else {
return null;
}
}
/**
* 获取到单链表的节点的个数 (如果是带头节点的链表,需求不统计头节点)
*
* @param headNode 链表的头节点
* @return 返回有效节点的个数
*/
public Integer getEffectLength(HeroNode headNode) {
if (headNode == null) {
return 0;
}
HeroNode temp = headNode.getNext();
if (temp == null) {
// 空链表
return 0;
}
int length = 0;
while (temp != null) {
length++;
temp = temp.getNext();
}
return length;
}
/**
* 查找单链表中的倒数第k个结点 【新浪面试题】
* <p>
* 思路
* <p>
* 1. 编写一个方法,接收head节点,同时接收一个index
* <p>
* 2. index 表示是倒数第index个节点
* <p>
* 3. 先把链表从头到尾遍历,得到链表的总的长度 getLength
* <p>
* 4. 得到size 后,我们从链表的第一个开始遍历 (size-index)个,就可以得到
* <p>
* 5. 如果找到了,则返回该节点,否则返回nulll
*
* @param headNode
* @param index
* @return
*/
public HeroNode findLastIndexNode(HeroNode headNode, Integer index) {
if (headNode == null) {
return null;
}
// 定义辅助变量
HeroNode temp = headNode.getNext();
if (temp == null) {
// 空链表
return null;
}
// 第一次遍历得到链表的有效长度
Integer effectLength = getEffectLength(headNode);
// 第二次遍历 size -index 位置就是我们倒数的第 k 个节点
if (index < 0 || index > effectLength) {
return null;
}
for (int i = 0; i < effectLength - index; i++) {
temp = temp.getNext();
}
return temp;
}
/**
* 将单链表反转
*
* @param heroNode 链表的头节点
*/
public void reverseList(HeroNode heroNode) {
if (heroNode == null) {
return;
}
//定义一个辅助的指针(变量) ,帮助我们遍历原来的链表
HeroNode cur = heroNode.getNext();
if (cur == null || cur.getNext() == null) {
// 如果当前链表为空, 或 只有一个节点,则无需反转,直接返回
return;
}
// 指向当前节点 【cur】 的下一个节点
HeroNode next;
HeroNode reverseHead = new HeroNode(0, "", "");
while (cur != null) {
//先暂时保存当前节点的下一个节点,因为后面需要使用
next = cur.getNext();
//将cur的下一个节点指向新的链表的最前端
cur.setNext(reverseHead.getNext());
// 将 cur 链接到新的链表上
reverseHead.setNext(cur);
// 让 cur 后移
cur = next;
}
heroNode.setNext(reverseHead.getNext());
}
/**
* 可以利用栈这个数据结构,将各个节点压入到栈中,然后利用栈的先进后出的特点,就实现了逆序打印的效果
* <p>
* 不改变原链表结构
*/
public void reversePrint(HeroNode heroNode) {
if (heroNode == null) {
// 空链表 , 不打印
return;
}
// 创建一个站栈 ,将各个节点压入栈
Stack<HeroNode> stack = new Stack<>();
HeroNode temp = heroNode.getNext();
while (temp != null) {
// 将链表的所有节点压入栈
stack.push(temp);
// temp 后移,
temp = temp.getNext();
}
// 将栈中的节点进行打印 , pop 出栈
while (stack.size() > 0) {
// 栈的特点时 先进后出
System.out.println(stack.pop());
}
}
}
/**
* 英雄类
*/
class HeroNode {
/**
* 英雄编号
*/
private int no;
/**
* 英雄姓名
*/
private String name;
/**
* 英雄称号
*/
private String nickName;
/**
* 指向下一个节点【英雄】
*/
private HeroNode next;
public HeroNode() {
}
public HeroNode(int no, String name, String nickName, HeroNode next) {
this.no = no;
this.name = name;
this.nickName = nickName;
this.next = next;
}
public HeroNode(int no, String name, String nickName) {
this.no = no;
this.name = name;
this.nickName = nickName;
}
public int getNo() {
return no;
}
public void setNo(int no) {
this.no = no;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getNickName() {
return nickName;
}
public void setNickName(String nickName) {
this.nickName = nickName;
}
public HeroNode getNext() {
return next;
}
public void setNext(HeroNode next) {
this.next = next;
}
@Override
public String toString() {
return "HeroNode{" + "no=" + no + ", name='" + name + '\'' + ", nickName='" + nickName + '\'' +
// ", next=" + next +
'}';
}
}
单链表面试题(新浪、百度、腾讯)
单链表的常见面试题有如下:
1)求单链表中有效节点的个数
/**
* 获取到单链表的节点的个数 (如果是带头节点的链表,需求不统计头节点)
*
* @param headNode 链表的头节点
* @return 返回有效节点的个数
*/
public Integer getEffectLength(HeroNode headNode) {
if (headNode == null) {
return 0;
}
HeroNode temp = headNode.getNext();
if (temp == null) {
// 空链表
return 0;
}
int length = 0;
while (temp != null) {
length++;
temp = temp.getNext();
}
return length;
}
2)查找单链表中的倒数第k个结点 【新浪面试题】
/**
* 查找单链表中的倒数第k个结点 【新浪面试题】
* <p>
* 思路
* <p>
* 1. 编写一个方法,接收head节点,同时接收一个index
* <p>
* 2. index 表示是倒数第index个节点
* <p>
* 3. 先把链表从头到尾遍历,得到链表的总的长度 getLength
* <p>
* 4. 得到size 后,我们从链表的第一个开始遍历 (size-index)个,就可以得到
* <p>
* 5. 如果找到了,则返回该节点,否则返回nulll
*
* @param headNode
* @param index
* @return
*/
public HeroNode findLastIndexNode(HeroNode headNode, Integer index) {
if (headNode == null) {
return null;
}
// 定义辅助变量
HeroNode temp = headNode.getNext();
if (temp == null) {
// 空链表
return null;
}
// 第一次遍历得到链表的有效长度
Integer effectLength = getEffectLength(headNode);
// 第二次遍历 size -index 位置就是我们倒数的第 k 个节点
if (index < 0 || index > effectLength) {
return null;
}
for (int i = 0; i < effectLength - index; i++) {
temp = temp.getNext();
}
return temp;
}
3)单链表的反转【腾讯面试题,有点难度】
思路分析图解
/**
* 将单链表反转
*
* @param heroNode 链表的头节点
*/
public void reverseList(HeroNode heroNode) {
if (heroNode == null) {
return;
}
//定义一个辅助的指针(变量) ,帮助我们遍历原来的链表
HeroNode cur = heroNode.getNext();
if (cur == null || cur.getNext() == null) {
// 如果当前链表为空, 或 只有一个节点,则无需反转,直接返回
return;
}
// 指向当前节点 【cur】 的下一个节点
HeroNode next;
HeroNode reverseHead = new HeroNode(0, "", "");
while (cur != null) {
//先暂时保存当前节点的下一个节点,因为后面需要使用
next = cur.getNext();
//将cur的下一个节点指向新的链表的最前端
cur.setNext(reverseHead.getNext());
// 将 cur 链接到新的链表上
reverseHead.setNext(cur);
// 让 cur 后移
cur = next;
}
heroNode.setNext(reverseHead.getNext());
}
4)从尾到头打印单链表 【百度,要求方式1:反向遍历 。 方式2:Stack栈】
/**
* 可以利用栈这个数据结构,将各个节点压入到栈中,然后利用栈的先进后出的特点,就实现了逆序打印的效果
* <p>
* 不改变原链表结构
*/
public void reversePrint(HeroNode heroNode) {
if (heroNode == null) {
// 空链表 , 不打印
return;
}
// 创建一个站栈 ,将各个节点压入栈
Stack<HeroNode> stack = new Stack<>();
HeroNode temp = heroNode.getNext();
while (temp != null) {
// 将链表的所有节点压入栈
stack.push(temp);
// temp 后移,
temp = temp.getNext();
}
// 将栈中的节点进行打印 , pop 出栈
while (stack.size() > 0) {
// 栈的特点时 先进后出
System.out.println(stack.pop());
}
}