1.关于判断链表是否有环的问题
在解决这类问题上,我个人认为有两种办法:哈希及双指针
那先来看一下如何用哈希去解决这类问题
算法描述:遍历所有的链表的所有结点,将这些个结点全部存在hashset里面,直到下一个结点位null,则结束,如果当前结点已经存在于哈希表里,那么证明有环,反之就没有。
接下来看代码:
1.先定义一个链表类
class ListNode{
int val;
ListNode next;
ListNode(int x){
val=x;
next=null;
}
}
2.再定义一个实例链表测试的主函数
public static void main(String[] args) {
ListNode listNode=new ListNode(3);
ListNode listNode1=new ListNode(2);
ListNode listNode2=new ListNode(0);
ListNode listNode3=new ListNode(-4);
listNode.next=listNode1;
listNode1.next=listNode2;
listNode2.next=listNode3;
listNode3.next=listNode1;
boolean b = hasCycle(listNode);
System.out.println(b);
}
那么实际的链表应该是这样的:
image.png
3.从这个链表实例出发可以看到我们调用了hasCycle()方法去判断是否有环,这里先用hashSet去实现。
public static boolean hasCycle1(ListNode head){
//base判断
if(head==null||head.next==null){
return false;
}
//创建容器
Set set=new HashSet();
//循环出口
while(head!=null){
//判断如果包含结点,直接确定是循环连接
if(set.contains(head)){
return true;
}else{
//没有就往里添加
set.add(head);
}
//结点依次往下
head=head.next;
}
//找不到就返回fasle
return false;
}
4使用快慢指针去解题
public static boolean hasCycle(ListNode head){
// base判断
if(head==null||head.next==null){
return false;
}
//指定快慢结点
ListNode fast=head.next;
ListNode slow=head;
//循环出口
while(fast!=null&&fast.next!=null){
//如果快指针走过一圈回来碰到慢指针那肯定有环
if(fast.equals(slow)){
return true;
}
//快慢指针往下走
fast=fast.next.next;
slow=slow.next;
}
//找不到就返回fasle
return false;
}
2.关于链表反转的问题
两种方法:迭代和递归
1.首先看迭代
假设存在链表 1 → 2 → 3 → Ø,我们想要把它改成 Ø ← 1 ← 2 ← 3。
在遍历列表时,将当前节点的 next 指针改为指向前一个元素。由于节点没有引用其上一个节点,因此必须事先存储其前一个元素。在更改引用之前,还需要另一个指针来存储下一个节点。不要忘记在最后返回新的头引用!
public class ReverseListNode {
public static void main(String[] args) {
ReverseListNode reverseListNode = new ReverseListNode();
ListNode l1=new ListNode(3);
ListNode l2=new ListNode(2);
ListNode l3=new ListNode(0);
ListNode l4=new ListNode(-1);
l1.next=l2;
l2.next=l3;
l3.next=l4;
System.out.println(reverseListNode.reverseList(l1));
}
public static ListNode reverseList(ListNode head){
//首先定义一个空指针,这个空指针定义是要在最头部的
ListNode prev=null;
//然后将curr指向head
ListNode curr=head;
//一步步迭代,出口就是curr=null的时候
while(curr!=null){
//先拿到curr的下一个指针
ListNode temp=curr.next;
//然后把当前的结点指向prev,即指向前一个结点
curr.next=prev;
//更新curr和prev的值,把prev往后移一位
prev=curr;
//再把当前结点往后移一位
curr=temp;
//就这样依次迭代,直到curr==null的时候跳出
}
//然后返回prev
return prev;
}
}
运行结果:
ListNode{val=-1, next=ListNode{val=0, next=ListNode{val=2, next=ListNode{val=3, next=null}}}}
Process finished with exit code 0
最终完成了反转。
1.再看递归
递归的解法很难理解,
递归的两个条件:
终止条件是当前节点或者下一个节点==null
在函数内部,改变节点的指向,也就是 head 的下一个节点指向 head 递归函数那句
head.next.next = head
很不好理解,其实就是 head 的下一个节点指向head。
递归函数中每次返回的 cur 其实只最后一个节点,在递归函数内部,改变的是当前节点的指向。
public static ListNode reverseList1(ListNode head){
//先确定递归的终止条件
if(head==null||head.next==null){
return head;
}
//先拿到最后一个结点curr
ListNode curr = reverseList1(head.next);
//如果链表是 1->2->3->4->5,那么此时的cur就是5
//而head是4,head的下一个是5,下下一个是空
//所以head.next.next 就是5->4
head.next.next=head;
//防止链表循环,将head.next=null
head.next=null;
//每层递归函数都返回cur,也就是最后一个节点
return curr;
}
结果如下:
ListNode{val=-1, next=ListNode{val=0, next=ListNode{val=2, next=ListNode{val=3, next=null}}}}
Process finished with exit code 0
3.再来讨论一个很经典的约瑟夫问题吧,这曾经是我在恒生笔试的一道题,当时一脸懵,现在蹭热再好好讨论一下经典的约瑟夫问题
image.png
public class JosephTest {
public static void main(String[] args) {
Node first=null;
Node pre=null;
for (int i = 1; i <=41; i++) {
//如果是第一个结点
if(i==1){
first=new Node(i,null);
pre=first;
continue;
}
//如果不是第一个结点
Node newNode=new Node(i,null);
//让前一个结点指向这个新节点
pre.next=newNode;
//然后再让pre指针往后移
pre=newNode;
//如果位最后一个结点,则最后一个结点位第一个结点
if(i==41){
pre.next=first;
}
}
//count计数,模拟报数
int count =0;
//记录第一个结点
Node n=first;
//当前结点的前一个结点
Node before=null;
//循环结束的条件n==n.next
while(n!=n.next){
//模拟报数
count++;
//判断是不是3
if(count==3){
//删除
before.next=n.next;
//打印
System.out.println(n.val);
//重置
count=0;
//继续往下
n=n.next;
}else{
//如果不是3.那么继续往下
before=n;
n=n.next;
}
}
System.out.println("最后的元素"+n.val);
}
}
class Node{
int val;
Node next;
public Node(int val, Node next) {
this.val = val;
this.next = next;
}
@Override
public String toString() {
return "Node{" +
"val=" + val +
", next=" + next +
'}';
}
}
最终结果: