提问:数组和单链表有什么区别?
数组的存储位置是连续的,链表的存储位置是不连续的
数组访问数据速度快,但是增加和删除数据的效率低;链表则与其相反
数组存储内容就是数据,而链表存储内容是数据和下个结点的地址
数组的存储空间是静态的,连续分布的,导致存储空间会出现过剩或者溢出现象
链表的存储空间是动态分布的,只要内存空间尚有空闲,就不会产生溢出;链表中每个结点是可以动态变化的,空间利用率会更好
任务要求:对英雄人物的创建,删除,更新,查找的实现
主函数调用
HeroNode hero1 = new HeroNode(1, "宋江", "及时雨");
HeroNode hero2 = new HeroNode(2, "卢俊义", "玉麒麟");
HeroNode hero3 = new HeroNode(3, "吴用", "智多星");
HeroNode hero4 = new HeroNode(4, "李逵", "黑旋风");
HeroNode hero5 = new HeroNode(5, "林冲", "豹子头");
// 创建要给的链表
SingLeLinkedListDemo demo = new SingLeLinkedListDemo();
// 加入4个节点
demo.addByOrder(hero1);
demo.addByOrder(hero3);
demo.addByOrder(hero2);
demo.addByOrder(hero4);
demo.update(hero5);
demo.delete(hero1);
// 显示链表
demo.list();
// 显示英雄的个数
System.out.println(demo.heroCount(SingLeLinkedListDemo.getHead()));
System.out.println("**************************************");
// 删除对应的英雄观察来判断正确
demo.delete(SingLeLinkedListDemo.bottomNode(SingLeLinkedListDemo.getHead(),4));
demo.list();
System.out.println("**************************************");
// 单链表的反转[腾讯面试题]
demo.ReverseHero(SingLeLinkedListDemo.getHead());
demo.list();
System.out.println("**************************************");
demo.ReverseHero2(SingLeLinkedListDemo.getHead());
demo.list();
定义一个链表类,用来生成每一个结点
class HeroNode{
private int no;
private String name;
private String nickname;
public HeroNode next; // 指向下一个节点
// 无参/有参构造函数
public HeroNode(){}
public HeroNode(int no,String name,String nickname){
this.name = name;
this.no = no;
this.nickname = nickname;
}
// 获得编号的方法
public int GetNo(){
return no;
}
// 获得姓名的方法
public String GetName(){
return name;
}
// 获得外号的方法
public String GetNickname(){
return nickname;
}
// 显示方便,重写toString()方法
@Override
public String toString(){
return "HeroNode[no="+no+",name="+name+",nickname"+nickname+"]";
}
}
核心代码:具体实现算法
我分步骤进行讲述:
1.先初始化一个头节点,头节点不要动,不放具体数据
private static HeroNode head = new HeroNode(0,"","");
// 获得头结点
public static HeroNode getHead() {
return head;
}
// 计算英雄的个数(单链表的节点个数)
public static int heroCount(HeroNode heroNode){
int Length = 0;
if(head == null){
return 0;
}
HeroNode temp = head;
while(temp.next != null){
Length++;
temp = temp.next;
}
return Length;
}
2.添加英雄结点(默认添加到最后一个位置)
// 添加节点到单项链表
// 思路:当不考虑编号顺序时
// 1.找到当前链表对的最后节点
// 2.将最后这个节点的next指向新的节点
public void add(HeroNode heroNode){
// head节点不能动,我们需要用head结点来遍历链表
HeroNode temp = head;
// 遍历链表,找到最后一个结点
while(true){
if(temp.next == null){
break;
}
temp = temp.next;
}
// 当退出while循环时候,temp就指向最后一个节点
temp.next = heroNode;
}
3.添加英雄结点升级版(添加到指定位置)
public void addByOrder(HeroNode heroNode){
// head节点不能动,我们需要用head结点来遍历链表
HeroNode temp = head;
while(true){
if(temp.next == null){
break;
}
if(temp.next.GetNo() == heroNode.GetNo()){ // heroNode.GetNo() 英雄数据编号
throw new RuntimeException("传入英雄数据编号有误!");
}
// 找到指定位置 进行添加结点
if(temp.next.GetNo() > heroNode.GetNo()){
heroNode.next = temp.next;
temp.next = heroNode;
return;
}
temp = temp.next;
}
// 当退出while循环时候,temp就指向最后一个节点
heroNode.next = temp.next;
temp.next = heroNode;
}
4.更新英雄结点(根据英雄数据编号进行更新)
public void update(HeroNode heroNode){
// 判断链表是否为空
if(head.next == null){
System.out.println("链表为空~~");
return;
}
//找到需要修给的结点,根据编号修改该英雄结点数据
HeroNode temp = head;
while(true) {
if (temp.next == null) {
System.out.println("没有该编号的人物,无法更新");
return;
}
if (temp.next.GetNo() == heroNode.GetNo()) {
heroNode.next = temp.next.next;
temp.next = heroNode;
return;
}
temp = temp.next;
}
}
5.删除英雄结点(根据英雄数据编号进行删除)
public void delete(HeroNode heroNode){
// 判断链表是否为空
if(head.next == null){
System.out.println("链表为空~~");
return;
}
//找到需要修给的节点,根据编号修删除该英雄结点数据
HeroNode temp = head;
while (true){
if(temp.next == null){
System.out.println("没有该人物英雄~~");
return;
}
if(temp.next.GetNo() == heroNode.GetNo()){
if(temp.next.GetName() == heroNode.GetName() && temp.next.GetNickname() == heroNode.GetNickname()){
temp.next = temp.next.next;
return;
}else{
System.out.println("删除的该人物英雄对象不匹配~~");
return;
}
}
temp = temp.next;
}
}
6.显示全部链表数据
public void list(){
// 判断链表是否为空
if(head.next == null){
System.out.println("链表为空!");
return;
}
// 因为头节点不能动,因此需要一个辅助变量
HeroNode temp = head;
while (true){
if(temp.next == null){
break;
}
temp = temp.next;
System.out.println("链表数据:"+temp.toString());
}
}
面试题补充进该任务
1.查找单链表中倒数第K个节点[新浪面试题]
public static HeroNode bottomNode (HeroNode heroNode,int k){
if(heroCount(heroNode) < k){
return null;
}
int count = heroCount(heroNode) - k;
HeroNode temp = head.next;
while(count != 0){
count--;
temp = temp.next;
}
return temp;
}
2.单链表的反转[腾讯面试题]
/**
* 单链表的反转[腾讯面试题]
* 思路:
* 1.先定义一个节点reverseHead = new HeroNode();
* 2.从头到尾遍历原来的链表,每遍历一个节点就将其取出,并放在新的链表reverseHead的最前端
* 3.原来的链表的head.next = reverseHead.next
*/
// MyCode
public static HeroNode ReverseHero(HeroNode heroNode){
HeroNode reverseHead = new HeroNode(0,"","");
if(heroNode.next == null) return heroNode;
// 记录heroNode.next
HeroNode next= heroNode.next;
// 记录reverseHead.next
HeroNode reverseNext= reverseHead.next;
// 记录heroNode.next.next
HeroNode nextNext = heroNode.next.next;
while(next != null){
if(nextNext != null){
heroNode.next.next = reverseNext;
reverseHead.next = next;
heroNode.next = nextNext;
nextNext = nextNext.next;
}else{
heroNode.next.next = reverseNext;
reverseHead.next = next;
heroNode.next = nextNext;
}
next = heroNode.next;
reverseNext = reverseHead.next;
}
heroNode.next = reverseHead.next;
return heroNode;
}
// Teacher's Code
public static HeroNode ReverseHero2(HeroNode heroNode){
if(heroNode.next == null) return heroNode;
HeroNode reverseHead = new HeroNode(0,"","");
HeroNode next= heroNode.next; // 记录heroNode.next
HeroNode nextNext = heroNode.next; // 记录heroNode.next.next
while(next != null){
nextNext = nextNext.next;
next.next = reverseHead.next;
reverseHead.next = next;
next = nextNext;
}
heroNode.next = reverseHead.next;
return heroNode;
}
对于一个结点,有单链表一样有存储数据的data
,指向后方的 next 它拥有单链表的所有操作和内容。但是还有一个前驱节点 pre
单链表 Vs 双向链表
单链表,查找的方向只能是一个,而双向链表可以向前或者向后查找,灵活性大
单向链表不能自我删除,需要靠辅助节点,而双向链表则可以自我删除
public class DoubleLinkedList {
public static void main(String[] args) {
HeroNoded[] heroList= new HeroNoded[5];
// 进行测试
heroList[0] = new HeroNoded(1, "宋江", "及时雨");
heroList[1] = new HeroNoded(2, "卢俊义", "玉麒麟");
heroList[2] = new HeroNoded(3, "吴用", "智多星");
heroList[3] = new HeroNoded(4, "李逵", "黑旋风");
heroList[4] = new HeroNoded(5, "林冲", "豹子头");
// 添加英雄结点数据
DoubleLinkedListDemo demo = new DoubleLinkedListDemo();
for (int i = 0; i < heroList.length; i++) {
demo.addHero(heroList[i]);
}
// 更新英雄结点数据
demo.update(new HeroNoded(5, "鲁智深", "花和尚"));
DoubleLinkedListDemo.list();
System.out.println();
System.out.println();
// 删除节点
demo.delete(new HeroNoded(5, "鲁智深", "花和尚"));
DoubleLinkedListDemo.list();
}
}
/**
* DoubleLinkedList 管理我们的英雄
*/
class DoubleLinkedListDemo{
private static HeroNoded head = new HeroNoded(0,"","");
private boolean IsEmptyList(){
return head.next == null;
}
// 增加英雄 (根据no排序)
public void addHero(HeroNoded heroNoded){
if(heroNoded == null){
System.out.println("未传入英雄!!!");
return;
}
if(IsEmptyList()){
head.next = heroNoded;
heroNoded.pre = head;
return;
}
HeroNoded temp = head.next;
while(true){
if(temp.next == null) break;
if(temp.GetNo() == heroNoded.GetNo()){
throw new RuntimeException("传入英雄数据编号有误!");
}
// 加入的英雄结点的no 小于 双向链表中的某一个结点的no
if(heroNoded.GetNo() < temp.GetNo()){
temp.pre.next = heroNoded;
heroNoded.next = temp;
heroNoded.pre = temp.pre;
temp.pre = heroNoded;
return;
}
temp = temp.next;
}
// 当退出while循环时候,temp就指向最后一个节点
temp.next = heroNoded;
heroNoded.pre = temp;
}
// 更新功能:修改节点的信息即根据no编号来修改
public void update(HeroNoded heroNoded){
// 判断链表是否为空
if(IsEmptyList()){
System.out.println("链表为空~~");
return;
}
//找到需要修给的节点,根据编号修改
HeroNoded temp = head.next;
while(true) {
if (temp == null) {
System.out.println("没有该编号的人物,无法更新");
return;
}
if (temp.GetNo() == heroNoded.GetNo()) {
if(temp.next != null){
heroNoded.next = temp.next;
temp.next.pre = heroNoded;
}
heroNoded.pre = temp.pre;
temp.pre.next = heroNoded;
return;
}
temp = temp.next;
}
}
// 删除功能:从双向链表中删除一个节点的思路
public void delete(HeroNoded heroNoded){
// 判断链表是否为空
if(head.next == null){
System.out.println("链表为空~~");
return;
}
//找到需要修给的节点,根据编号修改
HeroNoded temp = head.next;
while (true){
if(temp == null){
System.out.println("没有该人物英雄~~");
return;
}
if(temp.GetNo() == heroNoded.GetNo()){
if(temp.GetName().equals(heroNoded.GetName()) && temp.GetNickname().equals(heroNoded.GetNickname())){
temp.pre.next = temp.next;
if(temp.next != null){
temp.next.pre = temp.pre;
}
return;
}else{
System.out.println("删除的该人物英雄对象不匹配~~");
return;
}
}
temp = temp.next;
}
}
// 显示链表
public static void list(){
// 判断链表是否为空
if(head.next == null){
System.out.println("链表为空!");
return;
}
// 因为头节点不能动,因此需要一个辅助变量
HeroNoded temp = head;
while (true){
if(temp.next == null){
break;
}
temp = temp.next;
System.out.println("链表数据:"+temp.toString());
}
}
}
/**
* 定义HeroNode,每个HeroNode对象就是一个节点
*/
class HeroNoded{
private int no;
private String name;
private String nickname;
public HeroNoded next; // 指向下一个节点
public HeroNoded pre; //指向上一个结点
// 无参/有参构造函数
public HeroNoded(){}
public HeroNoded(int no,String name,String nickname){
this.name = name;
this.no = no;
this.nickname = nickname;
}
// 获得编号的方法
public int GetNo(){
return no;
}
// 获得姓名的方法
public String GetName(){
return name;
}
// 获得外号的方法
public String GetNickname(){
return nickname;
}
// 显示方便,重写toString()方法
@Override
public String toString(){
return "HeroNode[no="+no+",name="+name+",nickname"+nickname+"]";
}
}
你会得到一个双链表,其中包含的节点有一个下一个指针、一个前一个指针和一个额外的 子指针 。这个子指针可能指向一个单独的双向链表,也包含这些特殊的节点。这些子列表可以有一个或多个自己的子列表,以此类推,以生成如下面的示例所示的 多层数据结构 。
给定链表的头节点 head ,将链表 扁平化 ,以便所有节点都出现在单层双链表中。让
curr
是一个带有子列表的节点。子列表中的节点应该出现在扁平化列表中的curr
之后 和curr.next
之前 。返回 扁平列表的
head
。列表中的节点必须将其 所有 子指针设置为null
。示例 1:
输入:head = [1,2,3,4,5,6,null,null,null,7,8,9,10,null,null,11,12] 输出:[1,2,3,7,8,11,12,9,10,4,5,6] 解释:输入的多级列表如上图所示。 扁平化后的链表如下图:
示例 2:
输入:head = [1,2,null,3] 输出:[1,3,2] 解释:输入的多级列表如上图所示。 扁平化后的链表如下图:
示例 3:
输入:head = [] 输出:[] 说明:输入中可能存在空列表。
提示:
- 节点数目不超过
1000
1 <= Node.val <= 105
栈遍历
/*
// Definition for a Node.
class Node {
public int val;
public Node prev;
public Node next;
public Node child;
};
*/
class Solution {
public Node flatten(Node head) {
if(head == null || (head.next == null && head.child == null)) return head;
else{
Stack<Node> st = new Stack<>();
Node node = head;
while(node.next != null || node.child != null || !st.empty()){
if(node.child == null && node.next == null && !st.empty()){
Node temp = st.pop();
temp.prev = node;
node.next = temp;
}else if(node.child == null){
node = node.next;
}else{
if(node.next != null){
st.push(node.next);
}
Node temp = node.child;
node.next = temp;
temp.prev = node;
node.child = null;
}
}
return head;
}
}
}
执行用时:0 ms, 在所有 Java 提交中击败了100.00%的用户
内存消耗:39.6 MB, 在所有 Java 提交中击败了29.70%的用户
如果把单链表的最后一个节点的指针指向链表头部,而不是指向NULL,那么就构成了一个单向循环链表,通俗讲就是让尾节点指向头结点。
单向环形链表应用场景:Josephu(约瑟夫、约瑟夫环)问题
构建一个单向的环形链表思路
1.先创建第一个节点,让first指向该节点,并形成环形
2.后面当我们每创建一个新的节点,就把该节点,加入到已有的环形链表中即可
遍历环形链表
1.先让一个辅助指针(变量)temp,指向firt节点
2.然后通过一个while循环遍历该环形链表即可temp…next=first结束
package linkedList;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class CircleSingleLinkedList {
public static void main(String[] args) {
final int nums = 25;
CircleSingleLinkedListDemo circle = new CircleSingleLinkedListDemo();
circle.addNode(nums);
circle.showNode();
System.out.println(circle.orderNode(1,2,nums));;
}
}
class CircleSingleLinkedListDemo{
private CircleNode firstNode;
// 添加节点
public void addNode(int nums){
// 校验nums值
String str = "^[1-9][0-9]*$";
Pattern pattern = Pattern.compile(str);
Matcher matcher = pattern.matcher(nums + "");
if(!matcher.find()){
System.out.println("输入num不正确!");
}
// 辅助节点
CircleNode temp = null;
// 创建nums个节点
for (int i = 1; i <= nums; i++) {
CircleNode circleNode = new CircleNode(i);
if(i == 1){
firstNode = circleNode;
temp = firstNode;
temp.next = firstNode;
}
else{
temp.next = circleNode;
temp = temp.next;
temp.next = firstNode;
}
}
}
// 根据用户的输入计算节点出圈的顺序
// startNo:开始数数的节点,countNums:数的次数 nums:一共有多少小孩在圈中
public String orderNode(int startNo, int countNums,int nums){
// 校验nums值
if(firstNode == null || startNo < 1 || startNo > nums){
System.out.println("输出数据有误!");
return "";
}else{
String str = "";
// temp指向开始节点的前一个节点,firstNode指向开始节点
while(firstNode.next.getNo() != startNo){
firstNode = firstNode.next;
}
CircleNode temp = firstNode;
firstNode = firstNode.next;
while(nums-- > 0){
int step = countNums;
while(step > 1){
firstNode = firstNode.next;
temp = temp.next;
step--;
}
str += firstNode.getNo() + " ";
firstNode = firstNode.next;
temp.next = firstNode;
}
return str;
}
}
// 显示节点
public void showNode(){
// 辅助节点
CircleNode temp = firstNode;
if(temp == null){
System.out.println("环形链表为空");
}
while(temp.next != firstNode){
System.out.println(temp);
temp = temp.next;
}
System.out.println(temp);
}
}
class CircleNode{
private int no;
public CircleNode next;
public CircleNode(int no) {
this.no = no;
}
public int getNo() {
return no;
}
public void setNo(int no) {
this.no = no;
}
public CircleNode getNext() {
return next;
}
public void setNext(CircleNode next) {
this.next = next;
}
@Override
public String toString() {
return "CircleNode{" +
"no=" + no +
'}';
}
}
给你一个链表的头节点
head
,判断链表中是否有环。如果链表中有某个节点,可以通过连续跟踪
next
指针再次到达,则链表中存在环。 为了表示给定链表中的环,评测系统内部使用整数pos
来表示链表尾连接到链表中的位置(索引从 0 开始)。注意:pos
不作为参数进行传递 。仅仅是为了标识链表的实际情况。如果链表中存在环 ,则返回
true
。 否则,返回false
。示例 1:
输入:head = [3,2,0,-4], pos = 1 输出:true 解释:链表中有一个环,其尾部连接到第二个节点。
示例 2:
输入:head = [1,2], pos = 0 输出:true 解释:链表中有一个环,其尾部连接到第一个节点。
示例 3:
输入:head = [1], pos = -1 输出:false 解释:链表中没有环。
提示:
- 链表中节点的数目范围是
[0, 104]
-105 <= Node.val <= 105
pos
为-1
或者链表中的一个 有效索引 。**进阶:**你能用
O(1)
(即,常量)内存解决此问题吗?
哈希表
/**
* Definition for singly-linked list.
* class ListNode {
* int val;
* ListNode next;
* ListNode(int x) {
* val = x;
* next = null;
* }
* }
*/
public class Solution {
public boolean hasCycle(ListNode head) {
HashSet<ListNode> set = new HashSet<>();
while(head != null){
if(set.contains(head)) return true;
set.add(head);
head = head.next;
}
return false;
}
}
快慢指针
/**
* Definition for singly-linked list.
* class ListNode {
* int val;
* ListNode next;
* ListNode(int x) {
* val = x;
* next = null;
* }
* }
*/
public class Solution {
public boolean hasCycle(ListNode head) {
ListNode fast = head,slow = head;
while(slow != null){
slow = slow.next;
if(fast.next != null){
fast = fast.next.next;
}else{
fast = fast.next;
}
if(slow == null || fast == null) return false;
if(slow == fast) return true;
}
return false;
}
}
执行用时:0 ms, 在所有 Java 提交中击败了100.00%的用户
内存消耗:42.3 MB, 在所有 Java 提交中击败了64.21%的用户
给定一个链表的头节点
head
,返回链表开始入环的第一个节点。 如果链表无环,则返回null
。如果链表中有某个节点,可以通过连续跟踪
next
指针再次到达,则链表中存在环。 为了表示给定链表中的环,评测系统内部使用整数pos
来表示链表尾连接到链表中的位置(索引从 0 开始)。如果pos
是-1
,则在该链表中没有环。注意:pos
不作为参数进行传递,仅仅是为了标识链表的实际情况。不允许修改 链表。
示例 1:
输入:head = [3,2,0,-4], pos = 1 输出:返回索引为 1 的链表节点 解释:链表中有一个环,其尾部连接到第二个节点。
示例 2:
输入:head = [1,2], pos = 0 输出:返回索引为 0 的链表节点 解释:链表中有一个环,其尾部连接到第一个节点。
示例 3:
输入:head = [1], pos = -1 输出:返回 null 解释:链表中没有环。
提示:
- 链表中节点的数目范围在范围
[0, 104]
内-105 <= Node.val <= 105
pos
的值为-1
或者链表中的一个有效索引**进阶:**你是否可以使用
O(1)
空间解决此题?
哈希表
/**
* Definition for singly-linked list.
* class ListNode {
* int val;
* ListNode next;
* ListNode(int x) {
* val = x;
* next = null;
* }
* }
*/
public class Solution {
public ListNode detectCycle(ListNode head) {
if(head == null) return null;
Set<ListNode> set = new HashSet<>();
while(head != null){
if(set.contains(head)){
return head;
}else{
set.add(head);
}
head = head.next;
}
return null;
}
}
快慢指针
分析图取自官方(a:未入环,紫色点是相遇点)
数学分析原理
快慢指针的速度我们设置为快指针是慢指针的两倍
数学推导
// 第一次相遇,在慢指针第一圈移动时,不管快指针在哪里,快指针总能追上慢指针
// 假设一圈为a+b,快指针和慢指针刚入环相差x,快指针每一步都可以多靠近慢指针一个点,所以一定会在a+b-x之前遇到,因为a+b-x
/**
* Definition for singly-linked list.
* class ListNode {
* int val;
* ListNode next;
* ListNode(int x) {
* val = x;
* next = null;
* }
* }
*/
public class Solution {
public ListNode detectCycle(ListNode head) {
if (head == null) {
return null;
}
ListNode slow = head, fast = head;
while (fast != null) {
slow = slow.next;
if (fast.next != null) {
fast = fast.next.next;
} else {
return null;
}
if (fast == slow) {
ListNode ptr = head;
while (ptr != slow) {
ptr = ptr.next;
slow = slow.next;
}
return ptr;
}
}
return null;
}
}
执行用时:0 ms, 在所有 Java 提交中击败了100.00%的用户
内存消耗:42 MB, 在所有 Java 提交中击败了20.90%的用户