本文转载自面试大总结之一:Java搞定面试中的链表题目
链表是面试中常出现的一类题目,本文使用java实现了面试中常见的链表相关题目。并且每个问题都使用了递归和非递归两种实现方式。
目录:
1.求单链表中节点的个数
2.将单链表反转:reverseList(遍历法),reverseListRec(递归法)
3.查找单链表中的倒数第K个结点(K>0):reGetKthNode
4.查找单链表的中间结点:getMiddleNode
5.从尾到头打印单链表:reversePrintListStack,reversePrintListRec(递归)
6.已知两个单链表pHead1,pHead2各自有序,把他们合并成一个链表依然有序:mergeSortedList,mergeSortedListRec
7.判断一个单链表中是否有环:hasCycle
8.判断两个单链表是否相交:isIntersect
9.求两个单链表相交的第一个节点:getFirstCommonNode
10.已知一个单链表中存在环,求进入环中的第一个节点:
getFirstNodeInCycle,getFirstNodeInCycleHashMap
11.给出一单链表头指针pHead和一节点指针pToBeDeleted,O(1)时间复杂度删除节点
代码:
package linkedListSummary;
import java.util.HashMap;
import java.util.Stack;
/**
* 面试中有关链表的题目
* 目录:
* 1.求单链表中节点的个数
* 2.将单链表反转
* 3.查找单链表中倒数第K个节点
* 4.查找链表的中间节点
* 5.从尾到头打印单链表
* 6.已知两个单链表各自有序,把他们合成一个有序链表
* 7.判断一个链表中是否有环
* 8.判断两个单链表是否相交
* 9.求两个单链表相交的第一个节点
* 10.已知一个单链表存在环,求进入环中的第一个节点
* 11.给出一个链表头指针和一个节点指针,O(1)时间复杂度删除节点
*/
public class Demo {
private static class Node{
int val;
Node next;
public Node(int val){
this.val = val;
}
}
/*
* 0.打印链表
*/
public static void printList(Node head){
while(head != null){
System.out.print(head.val +" ");
head = head.next;
}
System.out.println();
}
/*
* 1.求链表的节点个数,注意首先判断链表是否为空。时间复杂度O(N)
*/
public static int getListLength(Node head){
if(head == null){
return 0;
}
int len = 0;
Node cur = head;
while(cur != null){
len ++;
cur = cur.next;
}
return len;
}
/*
* 2.(1)反转链表(遍历法)
* 从头到尾遍历原链表,每遍历一个节点,将其摘下作为新链表的最前端。
* 注意:链表为空和只有一个节点的情况。
* 时间复杂度O(n)
*/
public static Node reverseList(Node head){
if(head == null || head.next == null){
return head;
}
Node reHead = null;
Node cur = head;
while(cur != null){
Node preCur = cur;//用preCur保存住对要处理节点的引用
cur = cur.next; //cur更新到 下一个节点,(因为下面要对next进行更改,所以先获取cur.next供下一次循环使用)
preCur.next = reHead; //更新要处理节点的next引用
reHead = preCur; //更新reHead节点。
}
return reHead;
}
/*
* 2.(2)反转链表(递归法)
* 递归的精髓在于你就默认reverseListRec(Node head)已经成功帮你解决了子问题了!但是别去想如何解决的。
* 现在只要处理当前的node和子问题之间的关系。(找出基本情况,给出递归的结束情况)
* 最后就能圆满解决整个问题。
*
*/
public static Node reverseListRec(Node head){
//基本情况(即问题很小了,不用再递归地往下分了,可以直接处理了)
if(head == null || head.next == null){
return head;
}
Node reHread = reverseListRec(head.next);
//在原序列中,head.next就是现在已经反转好的子序列的最后一个节点
head.next.next = head;//把head接在reHead 串的最后一个节点的后面
head.next = null; //防止循环链表
return reHread;
}
/*
* 3.查找单链表中的倒数第K个节点(K>0)
* 最简单的方法就是,先统计单链表中节点的个数,然后再找到第(n-k)个节点,即倒数第K个节点。
* 注意:链表为空,k=0,k=1,以及k大于链表节点数的情况,时间复杂度为O(N)。代码略
* 另一种思路:使用两个指针,先让前面的指针走到正向第K个节点,这样前后两个指针的距离差就是k-1,
* 之后两个指针一起向前走,前面的真挚走到最后一个节点时,后面的指针所指的节点就是倒数第K个节点。
*
*/
public static Node reGetKthNode(Node head,int k){
//这里要求k>0,即k从1开始,如果k==0或链表为空,则返回null
if(k ==0 || head == null){
return null;
}
//定义两个指针
Node q = head;//q在p的前面
Node p = head;
//首先让q先移动到第k个节点,使q与p的距离差为k-1;
while(k >1 && q != null){
q = q.next;
k --;
}
//当节点数小于k时,返回null
if(k >1 || q ==null){
return null;
}
//前后两个指针一起移动,当q指针指向最后一个节点时,p指针指向倒数第K个节点
while(q.next != null){
q = q.next;
p = p.next;
}
return p;
}
/*
* 3.(2)递归打印出倒数第K个节点的值
*/
static int level = 0;//表示从递归最内层往外返回的层数
public static void reGetKthNodeRec(Node head,int k){
if(head == null){
return ;
}
if(k ==1){
return ;
}
reGetKthNodeRec(head.next, k);
level ++;
//倒数第K层了,即倒数第K个节点
if(level == k){
System.out.println(head.val);
}
}
/*
* 4.查找单链表的中间节点
* 思路:和上一题思路相似,也是设置两个节点指针,只不过在这里是,两个指针一起走,一个一次移动两个节点,一个一次移动一个,
* 当快的指针到达链表结尾时,慢的指针就到达了链表中间
*/
public static Node getMiddleNode(Node head){
if(head == null || head.next == null){
return head;
}
//q在p 的前面
Node q = head;
Node p = head;
//前面的指针q每次走两步,后面的指针p每次走一步,直到q到达最后一个节点
while(q.next != null){
q = q.next;
p = p.next;
if(q.next != null){
q = q.next;
}
}
return p;
}
/*
* 5.从尾到头打印出单链表
* 对于这种颠倒顺序的问题,首先应该想到栈,先进后出。
* 所以,该题要么自己使用栈,要么让系统使用栈,也就是使用递归。
* 注意:链表为空的情况
* 时间复杂度O(N)
*/
//5.(1)自己使用栈实现
public static void reversePrintList(Node head){
if(head == null){
return;
}
Stack<Node> stack = new Stack<>();
//把单链表所有节点都压入栈
Node cur = head;
while(cur != null){
stack.add(cur);
cur = cur.next;
}
//从栈中弹出所有节点
while(!stack.isEmpty()){
cur = stack.pop();
System.out.print(cur.val+" ");
}
System.out.println();
}
/*
* 5.(2)递归解法:从尾到头打印单链表
*/
public static void reversePrintListRec(Node head){
if(head == null){
return;
}
reversePrintListRec(head.next);
System.out.print(head.val+" ");
}
/*
*6.(1) 已知两个单链表pHead1,pHead2各自有序,把他们合成一个依然有序的单链表
*
* 这个问题类似归并排序,注意:两个链表都为空或一个为空的情况。
* 需要O(1)的空间复杂度,O(max(len1,len2))的时间复杂度
*/
public static Node mergeSoertedList(Node head1,Node head2){
if(head1 == null && head2 == null){
return null;
}
if(head1 == null){
return head2;
}
if(head2 == null){
return head1;
}
//合并后链表的头节点
Node head = null;
//合并链表的当前节点
Node cur = null;
//确定哪一个为合并后链表的头节点
if(head1.val < head2.val){
cur = head1;
head1 = head1.next;//跳过已合并的节点
}else{
cur = head2;
head2 = head2.next;
}
//确定头节点
head = cur;
while(head1 != null && head2 != null){
if(head1.val < head2.val){
cur.next = head1;//把较小的节点合并进链表
head1 = head1.next;//跳过已经合并的节点
}else{
cur.next = head2;
head2 = head2.next;
}
cur = cur.next;//找到下一个准备合并的节点
}
//合并剩余的元素
if(head1 != null){
cur.next = head1;
}
if(head2 != null){
cur.next = head2;
}
return head;
}
/*
* 6.(2)递归解法:合并两个有序链表
*/
public static Node mergeSoertedListRec(Node head1,Node head2){
if(head1 == null){
return head2;
}
if(head2 == null){
return head1;
}
Node mergeHead = null;
//当前两个节点选择一个作为目前两个子链表的头
if(head1.val < head2.val){
mergeHead = head1;
//连接已经解决好的子问题
mergeHead.next = mergeSoertedListRec(head1.next, head2);
}else{
mergeHead = head2;
mergeHead.next = mergeSoertedListRec(head1, head2.next);
}
return mergeHead;
}
/*
* 7.判断一个单链表中是否有环
* 基本思路:使用两个指针,如果一个链表中有环,也就是说用一个指针去遍历,是永远走不到头的,
* 因此使用两个指针去遍历,一个指针一次走一步,另一个指针一次走两步,如果有环,两个指针一定会相遇。
* 时间复杂度O(N)
*/
public static boolean hasCycle(Node head){
Node fast = head;//快指针一次走两步
Node slow = head;//慢指针一次走一步
while(fast != null && fast.next != null){
fast = fast.next.next;
slow = slow.next;
if(fast == slow){//相遇,即存在环
return true;
}
}
return false;
}
/*
* 8.判断两个单链表是否相交
* 基本思路:如果两个链表相较于某一节点,那么该节点之后的所有节点都是两个链表所共有的,
* 因此,若相交,那么最后一个节点一定是共有的,否则,就不相交。
* 时间复杂度O(len1+len2),因为要使用一个节点保存最后节点,所以空间复杂度为O(1)
*/
public static boolean isIntersec(Node head1,Node head2){
if(head1 == null || head2 == null){
return false;
}
Node tail1 = head1;
//找到链表1的最后一个节点
while(tail1.next != null){
tail1 = tail1.next;
}
Node tail2 = head2;
//找到链表2 的最后一个节点
while(tail2.next != null){
tail2 = tail2.next;
}
//判断两个最后节点是否相等
if(tail2 == tail1){
return true;
}
return false;
}
/*
* 9.求两个链表相交的第一个节点,对第一个链表遍历,计算链表长度len1,同时保存最后一个节点的地址。
* 对第二个链表遍历,计算长度为len2,同时检查最后一个节点是否和第一个链表的最后一个节点相同,若相同,则相交,否则,不相交。
* 然后两个链表从头开始遍历,假设len1 大于len2,
* 那么将第一个链表的先遍历len1-len2个节点(在这len1-len2个节点中是不会存在相交节点的),此时两个链表到第一个相交节点的距离就是一样的了。
* 然后两个链表一起遍历(成对比较即可),直到两个节点地址相同。
* 时间复杂度O(len1 +len2)
*/
public static Node getFirstCommonNode(Node head1,Node head2){
if(head1 == null|| head2 == null){
return null;
}
int len1 = 1;
Node tail1 = head1;
while(tail1.next != null){
tail1 = tail1.next;
len1++;
}
int len2 = 1;
Node tail2 = head2;
while(tail2.next != null){
tail2 = tail2.next;
len2++;
}
if(tail2 != tail1){
return null;
}
//两个链表的最后一个节点相等,说明两个链表相交
tail1 = head1;
tail2 = head2;
//先将长的链表移动 到|len1-len2|个节点,是两个链表剩余长度一样,那么他们到相交节点的长度就是一样的
if(len1 > len2){
for(int i = 0;i <len1-len2;i++){
tail1 = tail1.next;
}
}else{
for(int i = 0;i<len2-len1;i++){
tail2 = tail2.next;
}
}
//两个链表一起向后遍历,直到找到相交点
while(tail1 != tail2){
tail1 = tail1.next ;
tail2 = tail2.next ;
}
return tail1;
}
/*
* 10.求进入环中的第一个节点,用快慢指针做
*/
public static Node getFirstNodeInCycle(Node head){
Node slow = head;//慢指针一次走一步
Node fast = head;//快指针一次走两步
//(1)先找到快慢指针相遇点
while(fast != null && fast.next != null){
fast = fast.next.next;
slow = slow.next;
if(slow == fast){
break;
}
}
//错误检查,这是没有环的坏情况
if(fast == null || fast.next == null){
return null;
}
//(2)现在,相遇点离环的开始处的距离等于链表头到环开始的距离
//这样,我们就把慢指针放在链表头,快指针保持在相遇点,
//然后同速度(一次都走一步)前进,再次相遇点就是环的开始处。
slow = head;
while(slow != fast){
slow = slow.next;
fast = fast.next;
}
//再次相遇就是环的开始处
return fast;
}
/*
* 10.(2)求进入环的第一个节点,用HashMap做一个无环的链表,它每个节点的地址都是不一样的,
* 但是如果有环,指针沿着链表移动,那么这个指针最终会指向一个已经出现过的地址,
* 以地址为哈希表的键,每出现一个地址,就将该地址对应的值设置为true,
* 那么当谋和键对应的值已经为true时,说明这个地址已经出现过了,并且这个节点就是环的第一个节点。直接返回即可。
*/
public static Node getFirstNodeInCycleHashMap(Node head){
HashMap<Node,Boolean> map = new HashMap<>();
while(head != null){
//原来的代码:if(map.get(head) == true)这句代码会抛出空指针异常,因为当map不存在head键时,返回null
//现在将其改为如下代码,对map返回值判断是否为空,不为空,说明该节点已经添加到map中了,是环开始的节点,直接返回该节点即可
if( map.get(head) != null){
//这个地址之前已经出现过,就是环的开始处
return head;
}else{
map.put(head, true);//其实不一定设为true,其实其他也可以,只要使相应的值不为null即可
head = head.next;
}
}
//head == null结束循环,此时head == null
return head;
}
/*
* 11.给出一单链表的头指针head和一节点指针toBeDeleted,O(1)时间复杂度删除节点toBeDeleted,
* 对于删除节点,我们普通的思路就是让该节点的前一个节点指向该节点的下一个节点,
* 这种情况需要遍历找到前一个节点,时间复杂度为O(N)。
* 对于链表,链表中的每一个节点结构都是一样的,所以我们可以把该节点的下一个节点的数据复制到该节点,然后删除下一个节点即可。
* 注意:最后一个节点的删除依然要使用普通方法,但是总体的时间复杂度为O(1)
*/
public static void delete(Node head,Node toBeDeleted){
if(toBeDeleted == null){
return;
}
//说明toBeDeleted不是最后一个节点
if(toBeDeleted.next != null){
toBeDeleted.val = toBeDeleted.next.val;//将下一个节点的数据复制到该节点
toBeDeleted.next = toBeDeleted.next.next;//该节点next指针指向的节点改为其原下一个节点next指针指向的节点
}else{
//说明toBeDeleted节点是最后一个节点
Node node = head;
while(node.next != toBeDeleted){//找到倒数第二个节点,即被删除节点(此时为最后一个节点)的前一个节点
node = node.next;
}
node.next = null;
}
}
public static void main(String[] args) {
Node n1 = new Node(1);
Node n2 = new Node(2);
Node n3 = new Node(3);
Node n4 = new Node(4);
Node n5 = new Node(5);
n1.next = n2;
n2.next = n3;
n3.next = n4;
n4.next = n5;
//0:打印链表
//printList(n1);
//1:求节点个数
//System.out.println("number : "+getListLength(n1));
/*
//2.1反转链表,并打印
Node reverseList = reverseList(n1);
printList(reverseList);
*/
/*
//2.2反转链表,并打印,递归
Node reverseListRec = reverseListRec(n1);
printList(reverseListRec);
*/
/*
//3.查找单链表中的倒数第K个节点(K>0)
Node reGetKthNode = reGetKthNode(n1, 4);
System.out.println("reKthNode: "+reGetKthNode.val);
*/
/*
//3.递归打印单链表中的倒数第K个节点(K>0)
reGetKthNodeRec(n1,4);
*/
/*
//4.查找中间节点
Node middleNode = getMiddleNode(n1);
System.out.println("middleNode :"+middleNode.val);
*/
/*
//5.(1)从尾到头打印单链表,使用栈
reversePrintList(n1);
*/
/*
//5.2从尾到头打印单链表,递归
reversePrintListRec(n1);
*/
/*
//6.合并两个链表
Node h1 = new Node(2);
Node h2 = new Node(4);
Node h3 = new Node(6);
Node h4 = new Node(8);
h1.next = h2;
h2.next = h3;
h3.next = h4;
Node p1 = new Node(1);
Node p2 = new Node(3);
Node p3 = new Node(5);
Node p4 = new Node(7);
p1.next = p2;
p2.next = p3;
p3.next = p4;
//6.(1)非递归解法
// Node mergeSoertedList = mergeSoertedList(h1, p1);
// printList(mergeSoertedList);
//6.2递归解法
Node mergeSoertedListRec = mergeSoertedListRec(h1, p1);
printList(mergeSoertedListRec);
*/
/*
//7.判断一个单链表是否有环
boolean hasCycle = hasCycle(n1);
System.out.println("hasCycle: "+hasCycle);
Node m1 = new Node(2);
Node m2 = new Node(4);
Node m3 = new Node(6);
Node m4 = new Node(8);
m1.next = m2;
m2.next = m3;
m3.next = m4;
m4.next = m2;//表明该链表有环
boolean hasCycle2 = hasCycle(m1);
System.out.println("hasCycle2: "+hasCycle2);
*/
/*
// 8.判断两个链表是否相交
Node h1 = new Node(2);
Node h2 = new Node(4);
Node h3 = new Node(6);
Node h4 = new Node(8);
h1.next = h2;
h2.next = h3;
h3.next = h4;
Node p1 = new Node(1);
Node p2 = new Node(3);
Node p3 = new Node(5);
Node p4 = new Node(7);
p1.next = p2;
p2.next = p3;
p3.next = p4;
boolean intersec = isIntersec(h1, p1);
System.out.println("intersec: "+intersec);
Node t1 = new Node(2);
Node t2 = new Node(4);
Node t3 = new Node(6);
Node t4 = new Node(8);
t1.next = t2;
t2.next = t3;
t3.next = t4;
Node r1 = new Node(1);
Node r2 = new Node(3);
Node r3 = new Node(5);
Node r4 = new Node(7);
r1.next = r2;
r2.next = r3;
r3.next = r4;
//给两个链表添加相同节点,使他们相交
Node rt1 = new Node(11);
Node rt2 = new Node(12);
Node rt3 = new Node(13);
r4.next = rt1;
t4.next = rt1;
rt1.next = rt2;
rt2.next = rt3;
boolean intersec2 = isIntersec(t1, r1);
System.out.println("intersec2: "+intersec2);
//9.求两个链表相交的第一个节点
Node firstCommonNode = getFirstCommonNode(r1, t1);
System.out.println("firstCommonNode: "+firstCommonNode.val);//11
*/
/*
//10.求进入环的第一个节点
Node m1 = new Node(1);
Node m2 = new Node(2);
Node m3 = new Node(3);
Node m4 = new Node(4);
m1.next = m2;
m2.next = m3;
m3.next = m4;
m4.next = m2;//表明该链表有环
Node firstNodeInCycle = getFirstNodeInCycle(m1);
System.out.println("firstNodeInCycle: "+firstNodeInCycle.val);//2
Node firstNodeInCycleHashMap = getFirstNodeInCycleHashMap(m1);
System.out.println("firstNodeInCycleHashMap: "+firstNodeInCycleHashMap.val);
*/
//11.给出一单链表的头指针head和一节点指针toBeDeleted,O(1)时间复杂度删除节点toBeDeleted,
delete(n1, n2);//删除n2
printList(n1);//打印删除后的链表
delete(n1, n5);//删除尾节点n5
printList(n1);//打印删除后的链表
}
}