剑指offer(链表)
03 从尾到头打印链表
题目描述
输入一个链表,按链表从尾到头的顺序返回一个ArrayList。
示例1:
输入
{67,0,24,58}
返回值
[58,24,0,67]
题解:
利用栈先入后出的特性
代码:
import java.util.ArrayList;
import java.util.Stack;
public class Solution {
public ArrayList<Integer> printListFromTailToHead(ListNode listNode) {
ArrayList<Integer> res = new ArrayList<>();
Stack<ListNode> stack=new Stack<ListNode>();
ListNode temp = listNode;
while(temp != null){
stack.push(temp);
temp = temp.next;
}
while(!stack.isEmpty()){
res.add(stack.pop().val);
}
return res;
}
}
复杂度
- 时间复杂度: O(N)
- 空间复杂度: O(N)
14 链表中倒数第k个节点
题目描述
输入一个链表,输出该链表中倒数第k个结点。
示例1
输入
1,{1,2,3,4,5}
返回值
{5}
题解
使用如图的快慢指针,首先让快指针先行k步,然后让快慢指针每次同行一步,直到快指针指向空节点,慢指针就是倒数第K个节点。
代码
public class Solution {
public ListNode FindKthToTail(ListNode head,int k) {
if(head == null || k == 0) return null;
ListNode tem = head;
ListNode res = head;
for(int i = 1;i <= k;i++){
if(tem == null) return null;
tem = tem.next;
}
while(tem != null){
tem = tem.next;
res = res.next;
}
return res;
}
}
复杂度
- 时间复杂度:O(N)
- 空间复杂度:O(1)
15 反转链表
题目描述
输入一个链表,反转链表后,输出新链表的表头。
示例1
输入
{1,2,3}
返回值
{3,2,1}
题解:
双指针遍历链表
代码:
public class Solution {
public ListNode ReverseList(ListNode head) {
if(head == null) return head;
ListNode pre = null;
ListNode res = head;
while(res != null){
ListNode temp = res.next;
res.next = pre;
pre = res;
res = temp;
}
return pre;
}
}
复杂度
- 时间复杂度:O(n), 遍历一次链表
- 空间复杂度:O(1)
16 合并两个有序的链表
题目描述
输入两个递增排序的链表,合并这两个链表并使新链表中的节点仍然是递增排序的。
示例1:
输入:
1->2->4, 1->3->4
输出:
1->1->2->3->4->4
限制:
0 <= 链表长度 <= 1000
题解
递归法:
代码
//非递归法
public class Solution {
public ListNode Merge(ListNode list1,ListNode list2) {
ListNode newHead = new ListNode(-1);
ListNode current = newHead;
while (list1 != null && list2 != null) {
if (list1.val < list2.val) {
current.next = list1;
list1 = list1.next;
} else {
current.next = list2;
list2 = list2.next;
}
current = current.next;
}
// 跳出while循环后,list1或list2还没有遍历完成;分情况讨论
if (list1 != null) current.next = list1;
if (list2 != null) current.next = list2;
return newHead.next;
}
}
//递归版
public class Solution {
public ListNode Merge(ListNode list1,ListNode list2) {
if(list1 == null) return list2;
if(list2 == null) return list1;
ListNode res = null;
if(list1.val < list2.val){
res = list1;
res.next = Merge(list1.next,list2);
}else{
res = list2;
res.next = Merge(list1,list2.next);
}
return res;
}
}
复杂度分析:
- 时间复杂度: O(M+N)
- 空间复杂度: O(1) 节点引用 dumdum , cur 使用常数大小的额外空间。
25 复杂链表的复制
题目的描述
请实现 copyRandomList 函数,复制一个复杂链表。在复杂链表中,每个节点除了有一个 next 指针指向下一个节点,还有一个 random 指针指向链表中的任意节点或者 null。
输入:head = [[7,null],[13,0],[11,4],[10,2],[1,0]]
输出:[[7,null],[13,0],[11,4],[10,2],[1,0]]
题解
哈希表:
利用哈希表的查询特点,考虑构建原链表节点和新链表对应节点的键值对映射关系,再遍历构建新链表各节点的 next
和 random
引用指向即可。
算法流程:
- 若头节点
head
为空节点,直接返回null
; - 初始化: 哈希表
map
, 节点cur
指向头节点; - 遍历链表:
- 建立新节点,并向
map
添加键值对 (原cur
节点, 新cur
节点) ; cur
遍历至原链表下一节点;
- 建立新节点,并向
- 构建新链表的引用指向,遍历链表:
- 构建新节点的
next
和random
引用指向; cur
遍历至原链表下一节点;
- 构建新节点的
- 返回值: 新链表的头节点
map[cur]
;
代码
class Solution {
public Node copyRandomList(Node head) {
if(head == null) return null;
Node cur = head;
Map<Node, Node> map = new HashMap<>();
// 3. 复制各节点,并建立 “原节点 -> 新节点” 的 Map 映射
while(cur != null) {
map.put(cur, new Node(cur.val));
cur = cur.next;
}
cur = head;
// 4. 构建新链表的 next 和 random 指向
while(cur != null) {
map.get(cur).next = map.get(cur.next);
map.get(cur).random = map.get(cur.random);
cur = cur.next;
}
// 5. 返回新链表的头节点
return map.get(head);
}
}
复杂度分析:
- 时间复杂度 O(N) : 两轮遍历链表,使用 O(N) 时间。
- 空间复杂度 O(N) : 哈希表 map 使用线性大小的额外空间。
36 两个链表的第一个公共节点
题解
-
浪漫相遇
我们使用两个指针 p1,p2 分别指向两个链表 pHeadA,pHeadB 的头结点
然后同时分别逐结点遍历,当 p1 到达链表 PheadA 的末尾时,重新定位到链表 pHeadB 的头结点;
当 p2 到达链表 pHeadB 的末尾时,重新定位到链表 pHeadA 的头结点。
这样,当它们相遇时,所指向的结点就是第一个公共结点(或者无公共节点-都为空节点)。 -
利用HashSet的性质。(空间复杂度太大了,不推荐)
代码
//浪漫相遇
public class Solution {
public ListNode FindFirstCommonNode(ListNode pHead1, ListNode pHead2) {
ListNode p1 = pHead1;
ListNode p2 = pHead2;
//有公共节点: p1=p2=公共节点;
//没有公共节点: p1=p1=null;
while(p1 != p2){
p1 = p1 == null ? pHead2 : p1.next;
p2 = p2 == null ? pHead1 : p2.next;
}
return p1;
}
}
复杂度
- 浪漫相遇
- 时间复杂度:O(M+N)
- 空间复杂度:O(1)
- Set
复杂度分析
- 时间复杂度:O(M+N)
- 空间复杂度:O(M+N)
55 链表中环的入口节点
题目描述
给一个链表,若其中包含环,请找出该链表的环的入口结点,否则,输出null。
题解
利用HashSet的不重复性质
代码
import java.util.HashSet;
public class Solution {
public ListNode EntryNodeOfLoop(ListNode pHead){
if(pHead == null) return null;
ListNode cur = pHead;
HashSet<ListNode> set = new HashSet<>();
while(cur != null){
if(!set.contains(cur)){
set.add(cur);
}else{
return cur;
}
cur = cur.next;
}
return null;
}
}
复杂度
- 时间复杂度:O(N)
- 空间复杂度: O(N)
56 删除链表中重复的节点
题目描述
在一个排序的链表中,存在重复的结点,请删除该链表中重复的结点,返回链表头指针。
- 重复的结点不保留
链表1->2->3->3->4->4->5 处理后为 1->2->3->4->5
- 重复的结点保留
链表1->2->3->3->4->4->5 处理后为 1->2->5
题解
-
重复的结点不保留
这是一个简单的问题,仅测试你操作列表的结点指针的能力。由于输入的列表已排序,因此我们可以通过将结点的值与它之后的结点进行比较来确定它是否为重复结点。如果它是重复的,我们更改当前结点的 next 指针,以便它跳过下一个结点并直接指向下一个结点之后的结点。 -
重复的节点保留
这里我们使用双指针的方式,定义a,b两个指针。
考虑到一些边界条件,比如1->1->1->2这种情况,需要把开头的几个1给去掉,我们增加一个哑结点,方便边界处理。
- 初始的两个指针如下:
- 将a指针指向哑结点
- 将b指针指向head(哑结点的下一个节点)
2.遍历链表 - 如果a指向的值不等于b指向的值,则两个指针都前进一位
- 否则,就单独移动b,b不断往前走,直到a指向的值不等于b指向的值。
注意,这里不是直接比较a.val==b.val
,这么比较不对,因为初始的时候,a指向的是哑结点,所以比较逻辑应该是这样:
a.next.val == b.next.val
当两个指针指向的值相等时,b不断往前移动,这里是通过一个while循环判断的,因为要过滤掉1->2->2->2->3重复的2。
那么整个逻辑就是两个while
代码
//1. 重复的节点保留
public ListNode deleteDuplicates(ListNode head) {
ListNode current = head;
while (current != null && current.next != null) {
if (current.next.val == current.val) {
current.next = current.next.next;
} else {
current = current.next;
}
}
return head;
}
//2. 重复的节点不保留
import java.util.HashSet;
public class Solution {
public ListNode deleteDuplication(ListNode pHead){
if(pHead == null || pHead.next == null ) return pHead;
ListNode head = new ListNode(0);
head.next = pHead;
ListNode a = head, b = pHead;
while(b != null && b.next != null) {
if(a.next.val != b.next.val){
a = a.next;
b = b.next;
}else{
while(b != null && b.next != null && a.next.val == b.next.val){
b = b.next;
}
a.next = b.next;
b = b.next;
}
}
return head.next;
}
复杂度分析
- 保留重复的节点
- 时间复杂度:O(n),因为列表中的每个结点都检查一次以确定它是否重复,所以总运行时间为 O(n),其中 n 是列表中的结点数。
- 空间复杂度:O(1),没有使用额外的空间。
- 不保留重复的节点
- 时间复杂度:O(n)
- 空间复杂度:O(1)