在链表的学习过程中,为了加深对链表相关知识的巩固与理解,因此做了一些LeetCode上的题。一下都是一些比较典型的链表相关的编程题,同时附上解题思想,一方面作为自己的学习总结,另一方面也帮助大家更好的理解一些编程思想。
1、删除链表中等于给定值 val 的所有节点
思想:遍历链表,先处理头节点head为val的所有情况,然后再处理中间节点为val的所有情况。
若head.val=val,则让head=head.next, 此处用循环判断的目的是防止链表前不止一个节点的值为val,
在处理中间节点为val的情况,cur为我们设置的当前节点的引用,cur从头开始遍历链表,若cur.next=val,则让cur.next=cur.next.next;此处注意两点,因为我们之前已经处理过头节点为val的情况,因此此时再遍历链表的时候,头节点的值已经不可能为val,因此cur从头开始遍历的时候,我们只需关注cur.next是否为val了,第二点要注意的是,当cur.next=val时,我们要删除cur.next节点的时候需要做操作cur.next=cur.next.next,因此就需要对cur.next进行判空,否则就会抛出空指针异常。
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode(int x) { val = x; }
* }
*/
class Solution {
public ListNode removeElements(ListNode head, int val) {
if(head==null){
return null;
}
while(head.val==val){
if(head.next!=null)
head=head.next;
else
return null;
}
//二次判空是为了防止整个链表的所有结点值都等于val的情况
if(head==null){
return null;
}
//此时已处理了头节点为val的所有情况
ListNode cur=head;
while(cur.next!=null){
if(cur.next.val==val){
cur.next=cur.next.next;
}else{
cur=cur.next;
}
}
return head;
}
}
2、反转一个单链表
反转一个单链表可以有两种思想:
(1)先定义一个头结点的引用,引用对象为新链表的头,之后再对原链表进行头删,头删的元素再头插到新链表中,由此便可对原链表进行反转。
(2)定义3个引用分别为p1,p2,p3,刚开始让p1指向null,p2指向原链表的头,p3=p2.next;为了方便理解,下面画图来解释,
每次让p3=p2.next,再让p2.next=p1,最后再让p1,p2,p3向后移
最后我们只需再判断p2==null,然后返回p1即可。
考虑到效率问题,我们直接用第二种思路实现单链表的反转(我们刚开始让p1=head,其实并不影响,只需最后让尾节点的next为null即可)
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode(int x) { val = x; }
* }
*/
class Solution {
public ListNode reverseList(ListNode head) {
if(head==null||head.next==null){
return head;
}
ListNode p1=head;
ListNode p2=head.next;
ListNode p3=null;
while(p2!=null){
p3=p2.next;
p2.next=p1;
p1=p2;
p2=p3;
}
head.next=null; //尾节点置空
head=p1;
return head;
}
}
3、给定一个带有头结点 head 的非空单链表,返回链表的中间结点。如果有两个中间结点,则返回第二个 中间结点。
思想:采用快慢指针的方式,定义两个引用(这里我们用指针来描述)fast和slow,刚开始fast和slow都指向链表的头节点,之后让快指针一次走两步,慢指针一次走一步,当快指针走到链表尾的时候,慢指针正好走到链表中间的位置,这个时候慢指针所指向的节点就是我们要求的中间节点了。
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode(int x) { val = x; }
* }
*/
class Solution {
public ListNode middleNode(ListNode head) {
if(head==null){
return null;
}
ListNode fast=head;
ListNode slow=head;
//为避免出现对空指针解引用,因此我们需要对fast和fast.next都进行判空操作
while(fast!=null&&fast.next!=null){
fast=fast.next.next;
slow=slow.next;
}
return slow;
}
}
4、 输入一个链表,输出该链表中倒数第k个结点
思想:思想与上一题相同,既然要找链表的倒数第k个节点,我们只需让快指针先走k步,再让快指针和慢指针每次各走一步,那么当快指针走到链表尾的时候,慢指针所指向的位置就是链表的倒数第k个节点。
public class Solution {
public ListNode FindKthToTail(ListNode head,int k) {
ListNode fast=head;
ListNode slow=head;
while((k--)!=0){
if(fast==null){
return null;
}else
fast=fast.next;
}
while(fast!=null){
fast=fast.next;
slow=slow.next;
}
return slow;
}
}
5、将两个有序链表合并为一个新的有序链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的
思想:同时遍历两个链表,将其中较小元素的节点插入到新链表的尾部,该链表的指针向后走,新链表的指针向后走,当其中任一链表遍历完后,再将剩下的未遍历完的链表插入到新链表的尾部即可。
class Solution {
public ListNode mergeTwoLists(ListNode l1, ListNode l2) {
if(l1==null&&l2==null){
return null;
}
ListNode newNode=null; //指向新链表的头节点
ListNode cur=newNode; //指向新链表的当前节点
ListNode node1=l1; //指向链表1的当前节点
ListNode node2=l2; //指向链表2的当前节点
while((node1!=null)&&(node2!=null)){
if(node1.val<=node2.val){
//判断新链表的头节点是否为空
if(newNode==null){
newNode=node1;
}else{
cur.next=node1;
}
cur=node1;
node1=node1.next;
}else{
if(newNode==null){
newNode=node2;
}else{
cur.next=node2;
}
cur=node2;
node2=node2.next;
}
}
//将未遍历完的链表插入到新链表的尾部
if(node1!=null){
//防止一开始链表2就为null的情况
if(newNode==null){
return node1;
}
cur.next=node1;
}
if(node2!=null){
//防止一开始链表1就为null的情况
if(newNode==null){
return node2;
}
cur.next=node2;
}
return newNode;
}
}
6、编写代码,以给定值x为基准将链表分割成两部分,所有小于x的结点排在大于或等于x的结点之前
思想:创建两个新链表,其头结点分别为small,big;两个新链表对应的尾节点分别为slast和blast,遍历原链表,当原链表当前节点的值<=x时,将该节点尾插到头节点为small的链表中,当原结点的值>x时,将该节点尾插到头节点为big的链表中,遍历完之后若small链表不为null,再将big链到small链表尾部,返回small即可,若small为空,直接返回big即可;需要注意的是,一定要确保新链表尾节点的next值一定为null,否则会报错(之前在这块出了问题)。
public class Partition {
public ListNode partition(ListNode pHead, int x) {
if(pHead==null||pHead.next==null){
return null;
}
ListNode small=null; //小于等于x的节点组成的新链表的头节点
ListNode slast=small; //小于等于x的节点组成的新链表的尾节点
ListNode big=null; //大于x的节点组成的新链表的头节点
ListNode blast=big; //大于x的节点组成的新链表的尾节点
ListNode cur=pHead; //原链表的当前节点
while(cur!=null){
if(cur.val<x){
if(small==null){
small=cur;
}else{
slast.next=cur;
}
slast=cur;
cur=cur.next;
}else{
if(big==null){
big=cur;
}else{
blast.next=cur;
}
blast=cur;
cur=cur.next;
}
}
//确保尾节点为null
if(blast!=null){
blast.next=null;
}
if(small==null){
return big;
}else{
slast.next=big;
return small;
}
}
}
7、 在一个排序的链表中,存在重复的结点,请删除该链表中重复的结点,重复的结点不保留,返回链表头 指针
思想:创建一个新链表的头节点,遍历原链表,当原链表的当前值和下一个节点的值相等时当前节点往后走,不相等时尾插到新链表的结尾(这里注意,若刚开始没有遍历到重复节点,则直接将cur的值尾插到新链表中,若遍历到了重复节点,循环cur向后走,当cur.val!=cur.next.val出循环,此时需让cur再向后走一次才能尾插到新链表中,避免最后一个重复元素不能删除的情况)
注意点:如果链表中只有一个节点,则不存在重复节点,直接返回该链表头即可
public class Solution {
public ListNode deleteDuplication(ListNode pHead)
{
//先对链表判空
if(pHead==null){
return null;
}
//链表中只有一个节点
if(pHead.next==null){
return pHead;
}
ListNode head=new ListNode(0); //先定义一个假的头节点作为新链表的头
ListNode last=head; //新链表的尾
ListNode cur=pHead;//原链表的当前节点
while(cur!=null){
//如果当前节点和当前节点的下一个节点值重复,指针一直向后走
if(cur.next!=null&&cur.val==cur.next.val){
while(cur.next!=null&&cur.val==cur.next.val){
cur=cur.next;
}
cur=cur.next;//避免最后一个重复节点不能删除
}else{ //否则将该节点尾插到新链表的尾
last.next=cur;
last=last.next;
cur=cur.next;
}
}
last.next=null; //链表的最后要置空
return head.next;
}
}
8、链表的回文结构
对于一个链表,请设计一个时间复杂度为O(n),额外空间复杂度为O(1)的算法,判断其是否为回文结构。
思想:这里要利用栈的后进先出的特征。定义一个存储节点类型的栈,从头到尾遍历原链表,将原链表的每一个节点入栈,再次遍历链表,看原链表的当前元素值是否与栈顶元素值相同,若不同直接返回false,若相同,栈中元素出栈,原链表中指向当前元素的指针向后走,直到栈为空,此时链表也遍历到了最后一个元素,则证明该链表为回文结构。
改进:因为回文结构是两边对称相等的,因此我们可以考虑让链表的一半入栈即可,利用前面快慢指针的思想,找到链表的中间节点,然后从该节点开始让其之后的元素依次入栈,那么出栈的时候只要栈中元素出完了,便认为该链表为回文结构,这种方法降低了算法的时间复杂度,提升了效率。
public class PalindromeList {
public boolean chkPalindrome(ListNode A) {
Stack<ListNode> stack=new Stack<ListNode>();
if(A==null&&A.next==null){
return true;
}
ListNode fast=A;
ListNode slow=A;
while(fast.next!=null&&fast.next.next!=null){
fast=fast.next.next;
slow=slow.next;
}
while(slow!=null){
stack.push(slow);
slow=slow.next;
}
while(!stack.isEmpty()){
if(A.val!=stack.pop().val){
return false;
}else{
A=A.next;
}
}
return true;
}
}
9、 输入两个链表,找出它们的第一个公共结点。
思想:分别计算出两个链表的长度,得到他们的长度差L,然后让较长链表的指针先走L不,再让两个指针一同出发,相遇时的节点便为他们的第一个公共节点。
画图看更明显
链表1的长度为6,链表2的长度为5,链表1的当前节点p1先走一步到蓝色节点位置,然后两个链表的指针p1,p2同时走,相遇时即为第一个公共节点。
public class Solution {
public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
if(headA==null||headB==null){
return null;
}
ListNode p1=headA;
ListNode p2=headB;
int a=0;
int b=0;
while(p1!=null){
a++;
p1=p1.next;
}
while(p2!=null){
b++;
p2=p2.next;
}
int tmp=(b-a)>0?(b-a):(a-b);
while(tmp!=0){
if(a>b){
headA=headA.next;
}else{
headB=headB.next;
}
tmp--;
}
while(headA!=null&&headB!=null){
if(headA==headB){
return headA;
}
headA=headA.next;
headB=headB.next;
}
return null;
}
}
10、给定一个链表,判断链表中是否有环
思想:定义两个指针快指针和慢指针,两个指针都从头开始,快指针一次走两步,慢指针一次走一步,若链表带环,两个指针一定能在环内相遇,若没带环,则一定有个出口,以此判断链表是否带环。
public class Solution {
public boolean hasCycle(ListNode head) {
if(head==null||head.next==null){
return false;
}
ListNode fast=head;
ListNode slow=head;
while(fast.next!=null&&fast.next.next!=null){
fast=fast.next.next;
slow=slow.next;
if(fast==slow){
return true;
}
}
return false;
}
}
11、 给定一个链表,返回链表开始入环的第一个节点。 如果链表无环,则返回 NULL
思想:在判断链表有环的基础上,找到环的入口节点,同样利用快慢指针的思想找到两个指针的相遇点cur,找到之后再让一个指针从头开始走,我们假设是fast,fast从头开始走,一次走一步,fast继续从相遇点开始走,一次走一步,当两个指针再次相遇时相遇点便为环的入口点。
证明如下:
用S表示任一时刻慢指针所走的距离,F表示任一时刻快指针所走的距离,环的长度H,两个指针相遇时满足:S2=F;并且F-S=nH (即相遇时快指针比慢指针多走了n个环的长度),为方便计算我们取n=1,也就是说相遇时慢指针走的步数正好是环的长度,如图(图比较丑,大概也能看),假设在m点相遇,由上式推理我们可知a点到b点的距离就等于m点到b点的距离(这里指优弧的长度),当在m点相遇后,让fast从a点出发,slow从m点出发,当下次相遇时一定在b点,即为环的入口点。
public class Solution {
public ListNode detectCycle(ListNode head) {
boolean flag=ishuan(head);
if(flag==true){
ListNode fast=head;
ListNode slow=head;
int n=0;
while(true){
fast=fast.next.next;
slow=slow.next;
n++;
if(fast==slow){
break;
}
}
//fast从头出发
fast=head;
while(n!=0){
fast=fast.next;
n--;
}
slow=head;
while(fast!=slow){
fast=fast.next;
slow=slow.next;
}
return fast;
}
return null;
}
//判断是否为环
public boolean ishuan(ListNode head){
if(head==null||head.next==null){
return false;
}
ListNode fast=head;
ListNode slow=head;
while(fast.next!=null&&fast.next.next!=null){
fast=fast.next.next;
slow=slow.next;
if(fast==slow){
return true;
}
}
return false;
}
}
12、给定一个链表,每个节点包含一个额外增加的随机指针,该指针可以指向链表中的任何节点或空节点。 要求返回这个链表的深度拷贝。
思想:我们可以利用映射的关系,将原链表节点与新链表节点一一映射,这样就可通过原链表节点找到新链表节点,从而通过映射建立起指针的相互关系。映射我们可以通过map来实现。 map的key为原链表节点,value为新链表节点。通过原链表key获得新链表value将其返回即可。
class Solution {
public Node copyRandomList(Node head) {
if(head==null){
return null;
}
//key为原链表节点,value为新链表节点
Map<Node,Node> map=new HashMap<>();
Node cur=head;
while(cur!=null){
//复制原链表的每个节点给新链表的当前节点
map.put(cur,new Node(cur.val));
cur=cur.next;
}
//从原链表的头开始,建立指针的映射
cur=head;
while(cur!=null){
//将原链表的next映射到新链表的next,建立起next指针的相互关系
map.get(cur).next=map.get(cur.next);
//将原链表的random映射到新链表的random,建立起random指针的相互关系
map.get(cur).random=map.get(cur.random);
cur=cur.next;
}
return map.get(head);
}
}