002-链表
一、单链表
1.1 链表(Linked List)介绍
🔶 链表是有序的列表,但是它在内存中是存储如下:
- 链表是以节点的方式来存储,是链式存储。
- 每个节点包含 data 域, next 域:指向下一个节点。
- 如图:发现链表的各个节点不一定是连续存储。
- 链表分带头节点的链表和没有头节点的链表,根据实际的需求来确定。
🔶 链表(带头结点) 逻辑结构示意图如下:
1.2 单链表的应用实例
题目:使用带head头的单向链表实现【水浒英雄排行榜管理】,完成对英雄人物的增删改查操作。
- 第一种方法在添加英雄时,直接添加到链表的尾部。
- 第二种方式在添加英雄时,根据排名将英雄插入到指定位置(如果有这个排名,则添加失败,并给出提示)。
单链表代码实现
package com.atguigu.linkedlist;
public class SingleLinkedListDemo {
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 singleLinkedList = new SingleLinkedList();
singleLinkedList.addByOrder(hero3);
singleLinkedList.addByOrder(hero2);
singleLinkedList.addByOrder(hero4);
singleLinkedList.addByOrder(hero1);
singleLinkedList.list();
System.out.println("====================");
HeroNode newHero2 = new HeroNode(2, "小卢", "玉麒麟~~");
singleLinkedList.update(newHero2);
singleLinkedList.list();
System.out.println("====================");
singleLinkedList.delete(1);
singleLinkedList.delete(3);
singleLinkedList.delete(2);
singleLinkedList.delete(4);
singleLinkedList.list();
}
}
// 定义链表
class SingleLinkedList{
// 初始化头结点,不存放任何数据
private HeroNode head = new HeroNode(0,"","");
//返回头节点
public HeroNode getHead() {
return head;
}
// 向单链表结尾添加节点
public void add(HeroNode heroNode){
HeroNode t = head;
while(t.next!=null){
t = t.next;
}
t.next = heroNode;
}
// 显示/遍历 链表
public void list(){
if(head.next==null){
System.out.println("链表为空");
return;
}
HeroNode t = head.next;
while(t!=null){
System.out.println(t);
t = t.next;
}
}
//第二种方式在添加英雄时,根据排名将英雄插入到指定位置
//(如果已经有这个排名,则添加失败,并给出提示)
public void addByOrder(HeroNode heroNode){
HeroNode t = head;
while(t.next!=null){
if(t.next.no == heroNode.no){
System.out.println("添加失败,已经存在");
return;
}else if(t.next.no < heroNode.no){
t = t.next;
}else{ // 找到位置
heroNode.next = t.next;
break;
}
}
// heroNode.next = t.next; 语句放在这里也可以!
t.next = heroNode;
return;
}
// 修改节点信息,根据no编号来修改,no编号不可以修改
public void update(HeroNode heroNode){
/*if(head.next == null){
System.out.println("链表为空");
return;
}*/
HeroNode t = head.next;
while(t!=null){
if(t.no == heroNode.no){
t.name = heroNode.name;
t.nickname = heroNode.nickname;
return;
}
t = t.next;
}
System.out.println("没有找到该节点");
}
public boolean delete(int no){
/*if(head.next == null){
System.out.println("链表为空");
return false;
}*/
HeroNode t = head;
while(t.next != null){
if(t.next.no == no){
t.next = t.next.next; //被删除的节点,将不会有其它引用指向,会被垃圾回收机制回收
return true;
}
t = t.next;
}
return false;
}
}
// 定义链表节点
class HeroNode{
// 为什么用public? 因为private在类外不能被访问。需要Getter/Setter才能获取/修改。
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;
next = null;
}
@Override
public String toString(){
return "HeroNode [no=" + no + ", name=" + name + ", nickname=" + nickname + "]";
}
}
二、单链表面试题
1. 求单链表中有效节点的个数
显示代码
//获取单链表有效节点的个数
public static int GetNum(HeroNode head){
HeroNode t = head.next;
int res = 0;
while(t!=null){
res++;
t = t.next;
}
return res;
}
2. 查找单链表中的倒数第k个结点
【剑指offer 22】 答案
3. 单链表的反转
【leetcode 206】
4. 从尾到头打印单链表
【方式1:反向遍历 。 方式2:Stack栈】
5. 合并两个有序的单链表,合并之后的链表依然有序
【leetcode 21】
三、双向链表
3.1 单向链表的缺点分析:
- 单向链表,查找的方向只能是一个方向,而双向链表可以向前或者向后查找。
- 单向链表不能自我删除,需要靠辅助节点(待删除节点的前一个节点),而双向链表,则可以自我删除。
3.2 双向链表代码实现:
双向链表代码实现
package com.atguigu.linkedlist;
public class DoubleLinkedListDemo {
public static void main(String[] args) { System.out.println("双向链表的测试:"); HeroNode2 hero1 = new HeroNode2(1, "宋江", "及时雨"); HeroNode2 hero2 = new HeroNode2(2, "卢俊义", "玉麒麟"); HeroNode2 hero3 = new HeroNode2(3, "吴用", "智多星"); HeroNode2 hero4 = new HeroNode2(4, "林冲", "豹子头"); DoubleLinkedList d = new DoubleLinkedList(); d.addByOrder(hero4); d.addByOrder(hero2); d.addByOrder(hero3); d.addByOrder(hero1); d.list(); //修改 System.out.println("双向链表的update测试:"); HeroNode2 hero5 = new HeroNode2(4, "公孙胜", "入云龙"); d.update(hero5); d.list(); //删除 d.delete(3); System.out.println("双向链表的delete测试:"); d.list(); }
}
class DoubleLinkedList{
private HeroNode2 head = new HeroNode2(0,"",""); //返回头节点 (单/双链表相同) public HeroNode2 getHead() { return head; } // 显示/遍历 链表 (单/双链表相同) public void list(){ if(head.next==null){ System.out.println("链表为空"); return; } HeroNode2 t = head.next; while(t!=null){ System.out.println(t); t = t.next; } } // 向双链表结尾添加节点 public void add(HeroNode2 heroNode){ HeroNode2 t = head; while(t.next!=null){ t = t.next; } t.next = heroNode; heroNode.pre = t; } // 按排名顺序添加链表 public void addByOrder(HeroNode2 heroNode){ HeroNode2 t = head; while(t.next!=null){ if(t.next.no == heroNode.no){ System.out.println("添加失败,已经存在"); return; }else if(t.next.no < heroNode.no){ t = t.next; }else{ // 找到位置 break; } } heroNode.next = t.next; t.next = heroNode; heroNode.pre = t; if(heroNode.next != null){ heroNode.next.pre = heroNode; } } // 修改节点信息,根据no编号来修改,no编号不可以修改 public void update(HeroNode2 heroNode){ HeroNode2 t = head.next; while(t!=null){ if(t.no == heroNode.no){ t.name = heroNode.name; t.nickname = heroNode.nickname; return; } t = t.next; } System.out.println("没有找到该节点"); } public boolean delete(int no){ /*if(head.next == null){ System.out.println("链表为空"); return false; }*/ HeroNode2 t = head.next; while(t != null){ if(t.no == no){ t.pre.next = t.next; if(t.next!=null) { //要判断,t有可能是最后一个节点,next域为空 t.next.pre = t.pre; } return true; } t = t.next; } return false; }
}
class HeroNode2{
// 为什么用public? 因为private在类外不能被访问。需要Getter/Setter才能获取/修改。 public int no; public String name; public String nickname; public HeroNode2 next; //默认为空,不用修改 public HeroNode2 pre; public HeroNode2(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 + "]"; }
}
四、单向环形链表与约瑟夫问题
4.1 单向环形列表
4.2 Josephu 问题
Josephu问题描述:
设编号为1,2,… n 的 n 个人围坐一圈,约定编号为k(1<=k<=n)的人从1开始报数,数到 m 的那个人出列,它的下一位又从1开始报数,数到m的那个人又出列,依次类推,直到所有人出列为止,由此产生一个出队编号的序列。
解题思路1
用一个不带头结点的循环链表来处理 Josephu 问题:
先构成一个有n个结点的单循环链表,然后由 k 结点起从1开始计数,计到 m 时,对应结点从链表中删除,然后再从被删除结点的下一个结点又从1开始计数,直到最后一个结点从链表中删除算法结束。
单向环形链表实现
package com.atguigu.linkedlist;
public class Josephu {
public static void main(String[] args) { CircleSingleLinkedList c = new CircleSingleLinkedList(); c.addNote(125); c.show(); //测试小孩出圈 c.outCircle(10,20,125); }
}
// 环形单向链表
class CircleSingleLinkedList{private Node first = new Node(-1); // 构建环形链表 public void addNote(int nums){ if(nums < 1){ System.out.println("nums不正确!"); return; } Node cur = null; // 无需赋值构造函数,可以直接赋值? for(int i=1; i<=nums; i++){ Node newN = new Node(i); if(i==1){ first = newN; //first = newN; 与first.setNext(newN)区分 newN.setNext(first); //指向自身 cur = first; }else{ cur.setNext(newN); newN.setNext(first); cur = newN; } } } public void show(){ if(first == null){ System.out.println("链表为空"); return; } Node t = first; while(t.getNext()!=first){ // 遍历中止条件 System.out.printf("小孩编号 %d \n", t.getNo()); t = t.getNext(); } System.out.printf("小孩编号 %d \n", t.getNo()); } //出圈顺序 /* * @param startNo 表示从第几个小孩开始数数 * @param countNum 表示数几下 * @param nums 表示最初有多少小孩在圈中 * */ public void outCircle(int startNo, int countNum, int nums){ //先对数据校验 if(first == null || startNo < 1 || startNo > nums){ System.out.println("参数输入有误"); return; } // 创建辅助指针 Node helper = first; //需求创建一个辅助指针(变量) helper, 事先应该指向环形链表的最后这个节点(first的前面) while(helper.getNext()!=first){ helper = helper.getNext(); } //first移动到startNo位置,helper对应移动 for(int i=0; i<startNo-1; i++){ first = first.getNext(); helper = helper.getNext(); } //当小孩报数时,让first与helper指针同时移动countNum-1次,然后出圈。 while(true){ if(helper == first){ //圈中只剩一个人 break; } for(int j=0; j<countNum-1; j++){ first = first.getNext(); helper = helper.getNext(); } //此时first指向的是要出圈的小孩 System.out.printf("小孩 %d 出圈\n", first.getNo()); //helper.setNext(first.getNext()); //first = helper.getNext(); first = first.getNext(); helper.setNext(first); } System.out.printf("最后留在圈中的小孩编号 %d \n", first.getNo()); }
}
// 节点类
class Node{private int no; private Node next; Node(int no){ this.no = no; } public int getNo() { return no; } public void setNo(int no) { this.no = no; } public Node getNext() { return next; } public void setNext(Node next) { this.next = next; }
}
单向链表删除时,需要待删除节点的上一个节点信息,此处helper指针即为此作用,同时用于判断链表只有一个节点情况。
解题思路2
数学推导:
首先对于对于第一轮:
0, 1, 2, ......, M-2, M-1, M, M+1, ......, N-2, N-1 ---------------------------------数组1 (N个人)
剩下的人:
0, 1, 2, ......, M-2, M, M+1, ......, N-2, N-1
对剩下的人的排列换成下面这样:
M, M+1, M+2, ......, N-2, N-1, 0, 1, 2, ......, M-2 --------------------------------数组2 (N-1个人)
很容易发现上面的数组2等价于:
{(x+M)mod N | x = 0, 1, 2, ......, n-3, n-2} ------------------------------------数组3;
很明显如果知道了数组 2 中幸存者编号,那也等于知道了数组 1 中幸存者的编号;
而数组 2 等价于数组 3,所以现在问题变成了从 0 到 n-2 的人中找幸存者;即假设我们从0到 n-2 中找到了幸存者编号为 x,那么根据公式 (x + M)mod N 即可得到数组 2 中的幸存者编号,也即数组1 N 个人中的幸存者。
到此这个问题就是就可以总结为,如果知道了N-1个人中幸存者的编号,则可计算出N个人中幸存者的编号;
N应该大于等于1,而当 N=1 时,只有一个人,那幸存者编号就是 x1=0(从0开始计数);接着可以计算出 N=2 时的幸存者编号x2=(x1+M)%2,然后利用 N=2 可以计算出N=3 ... ...;
数学推导实现
public class JosephuMath {
public static void main(String[] args) {
int n = 5, m = 2;
System.out.println(josephu(n, m));
}
public static int josephu(int n, int m) {
int ret = 0, i;
for (i = 2; i <= n; i++) {
ret = (ret + m) % i;
}
return ret;
}
}
资料整理于 哔哩哔哩 尚硅谷韩顺平Java数据结构与算法