系列汇总:《刷题系列汇总》
——————《剑指offeer》———————
1. 两个链表的第一个公共结点
- 题目描述:输入两个无环的单链表,找出它们的第一个公共结点。(注意因为传入数据是链表,所以错误测试数据的提示是用其他方式显示的,保证传入数据是正确的)
- 优秀思路:将两个链表交换补充到对方的前面,然后逐位对比该位是否相同即可
public class Solution {
public ListNode FindFirstCommonNode(ListNode pHead1, ListNode pHead2) {
ListNode p1 = pHead1;
ListNode p2 = pHead2;
while(p1 != p2){
p1 = p1==null ? pHead2 : p1.next; // 相当于 pHead2 前补 pHead1
p2 = p2==null ? pHead1 : p2.next; // 相当于 pHead1 前补 pHead2
}
return p1;
}
}
2. 链表中环的入口结点
- 题目描述:给一个链表,若其中包含环,请找出该链表的环的入口结点,否则,输出
null
。 - 我的思路:利用哈希表存储已走过的结点,若遇到重复节点则该节点为入环节点
- 优秀思路:
-
第一步,找环中相汇点:定义一快p1、一慢p2两个节点,p1 一次走2步,p2 一次走1步,则p1、p2一定会在环上的某个点相汇聚。设入环前共A个节点,一个环B个节点,p2入环走了x个节点时相遇,p1 绕了n圈,则
2*(x+A)= (A+x) + nB
,则x = nB-A
。
(注意:n≥1,如果环前面的链表很长,而环短,那么快指针进入环以后可能转了好几圈才和慢指针相遇。但无论如何,慢指针在进入环的第一圈的时候就会和快的相遇。) -
第二步,找环的入口。将此时的p1指向头结点,速度调为1,p1、p2同时走,相遇时则为入环节点,证明如下:
相遇时 p2 在环上走了 nB-A 个节点,等效的环上位置为 B-A
还有 B-(B-A) = A 个节点到达入环起点 = 入环前的总结点数 -
public class Solution {
ListNode EntryNodeOfLoop(ListNode pHead){
// 边界条件
if(pHead == null || pHead.next == null) return null;
ListNode fast = pHead;
ListNode slow = pHead;
while(slow != null){
fast = fast.next.next;
slow = slow.next;
if(fast == slow){
fast = pHead;
while(fast != slow){
fast = fast.next;
slow = slow.next;
}
return fast;
}
}
return null;
}
}
3. 链表中倒数第k个结点
- 题目描述:输入一个链表,输出该链表中倒数第k个结点。如果该链表长度小于k,请返回空。
- 优秀思路:假设总共
n
个节点,倒数第k
个节点,其实就是正数n-k+1
个节点。利用双指针,其中一个指针p1
先走k
步,再让另一个指针p2
开始同时运动,当p1
指向末尾时,p2
指向的即为第n-k+1
个节点
public class Solution {
public ListNode FindKthToTail (ListNode pHead, int k) {
if(pHead == null) return null;
int index = 0;
ListNode first = pHead;
// p1 先行 k 步
while(first != null && index < k){
first = first.next;
index++;
}
if(index != k) return null;
ListNode second = pHead;
// p2 紧跟其后
while(first != null){
first = first.next;
second = second.next;
}
return second;
}
}
4. 反转链表
- 题目描述:输入一个链表,反转链表后,输出新链表的表头。
- 优秀思路:因为链表里每个节点都存储了下一个节点的地址,所以只需要让下一个节点存储上一个节点的地址即可,如下图
public class Solution {
public ListNode ReverseList(ListNode head) {
if(head == null || head.next == null) return head;
ListNode pre = null; // 当前节点的上一节点
ListNode next = null; // 当前节点的下一节点
// head 当前节点
while(head != null){
next = head.next; // 右移 下一节点
head.next = pre; // 当前节点指向上一节点
pre = head; // 右移 上一节点
head = next; // 右移 当前节点
}
return pre;
}
}
5. 合并两个排序的链表
- 题目描述:输入两个单调递增的链表,输出两个链表合成后的链表,当然我们需要合成后的链表满足单调不减规则。
- 优秀思路:利用归并排序的思想,主要是代码细节问题
public class Solution {
public ListNode Merge(ListNode list1,ListNode list2) {
if(list1 == null) return list2;
if(list2 == null) return list1;
ListNode newNode = new ListNode(0); // 建立首节点为0的节点
ListNode index = newNode; // 必须建立index指向newNode头结点
/* 因为不用反复调用list1 list2,所以容许list1 list2可变
ListNode p1 = list1;
ListNode p2 = list2;
*/
while(list1 != null && list2 != null){
if(list1.val < list2.val){
index.next = list1;
list1 = list1.next;
}else{
index.next = list2;
list2 = list2.next;
}
index = index.next; // 这步别忘了,不然sum一直指向的都是头部
}
// 一句就指向list1后续没读完的所有部分
if(list1 != null) index.next = list1;
if(list2 != null) index.next = list2;
// 不能输出index,只能输出newNode
return newNode.next; // 加next是为了不输出头结点0
}
}
6. 从尾到头打印链表
- 题目描述:输入一个链表,按链表从尾到头的顺序返回一个ArrayList。
- 我的思路:建立
ArrayList
,逐个add
链表元素值后,利用Collections
的reverse()
函数将ArrayList
倒序输出即可
import java.util.ArrayList;
import java.util.Collections;
public class Solution {
public ArrayList<Integer> printListFromTailToHead(ListNode listNode) {
ArrayList<Integer> list = new ArrayList<Integer>();
if(listNode == null) return list;
while(listNode != null){
list.add(listNode.val);
listNode = listNode.next; // 这句别忘了
}
Collections.reverse(list);
return list;
}
}
- 优秀思路1:在我的基础上便面了在倒一边序和调用库函数,直接将每次的链表元素值
add
到ArrayList
的0
索引处(最前面)即可,本来已存在的元素会自动后移(本题也可利用栈先进后出的特点实现)
import java.util.ArrayList;
public class Solution {
public ArrayList<Integer> printListFromTailToHead(ListNode listNode) {
ArrayList<Integer> list = new ArrayList<Integer>();
if(listNode == null) return list;
while(listNode != null){
list.add(0,listNode.val); // 核心!!!
listNode = listNode.next;
}
return list;
}
}
7. 复杂链表的复制(困难)
- 题目描述:输入一个复杂链表(每个节点中有节点值,以及两个指针,一个指向下一个节点,另一个特殊指针random指向一个随机节点),请对此链表进行深拷贝,并返回拷贝后的头结点。(注意,输出结果中请不要返回参数中的节点引用,否则判题程序会直接返回空)
- 优秀思路1:肯定不可以直接将节点赋值给新的节点,这样就是引用了。
- 新建节点,然后新节点的值跟原来的节点的一样。
- 但是要怎么存储呢?因为我们深拷贝的新的链表,每一个节点都是跟原来链表一一对应的,但是是互相独立的,所以我就想到了
HashMap
,它不就是存储k-v
键值对的吗,这样不就可以将两个节点一一对应起来。(这里的一一对应的意思是,知道哪个是哪个的深拷贝节点) - 接着,我们还需要将新链表中的每个节点赋上
next,random
属性的值,那么我们就可以通过hashmap
,通过key
(原链表的节点),取出拷贝的节点,然后将这个key
(旧节点)的两个属性值拷贝给新节点。
import java.util.*;
public class Solution {
public RandomListNode Clone(RandomListNode pHead){
HashMap<RandomListNode, RandomListNode> map = new HashMap<RandomListNode,RandomListNode>();
RandomListNode p = pHead;
// 第一次遍历,新建立节点
// map里面存储的键:原节点
// 值:一个和原节点值相同的新节点
while(p != null){
RandomListNode newNode = new RandomListNode(p.label);
map.put(p,newNode);
p = p.next;
}
// 回到头结点,进行第二次遍历,赋值对应的关系
p = pHead;
while(p != null){
// 不能是获取当前节点、next节点、random节点,都通过map.get()的方式获得,这个就保证了新链表中的每个节点都是新节点
RandomListNode node = map.get(p);
//
node.next = (p.next==null)? null : map.get(p.next);
node.random = (p.random==null)? null : map.get(p.random);
p = p.next;
}
return map.get(pHead);
}
}
-
优秀思路2:
- 遍历链表,复制每个结点,如复制结点A得到A1,将结点A1插到结点A后面;
- 重新遍历链表,复制老结点的随机指针给新结点,如A1.random = A.random.next;
- 拆分链表,将链表拆分为原链表和复制后的链表
public class Solution {
public RandomListNode Clone(RandomListNode pHead) {
if(pHead == null) {
return null;
}
RandomListNode currentNode = pHead;
//1、复制每个结点,如复制结点A得到A1,将结点A1插到结点A后面;
while(currentNode != null){
RandomListNode cloneNode = new RandomListNode(currentNode.label);
RandomListNode nextNode = currentNode.next;
currentNode.next = cloneNode;
cloneNode.next = nextNode;
currentNode = nextNode;
}
currentNode = pHead;
//2、重新遍历链表,复制老结点的随机指针给新结点,如A1.random = A.random.next;
while(currentNode != null) {
currentNode.next.random = currentNode.random==null? null : currentNode.random.next;
currentNode = currentNode.next.next;
}
//3、拆分链表,将链表拆分为原链表和复制后的链表
currentNode = pHead;
RandomListNode pCloneHead = pHead.next;
while(currentNode != null) {
RandomListNode cloneNode = currentNode.next;
currentNode.next = cloneNode.next;
cloneNode.next = cloneNode.next==null?null:cloneNode.next.next;
currentNode = currentNode.next;
}
return pCloneHead;
}
}
8. 删除链表中重复的结点
- 题目描述:在一个排序的链表中,存在重复的结点,请删除该链表中重复的结点,重复的结点不保留,返回链表头指针。 例如,链表
1->2->3->3->4->4->5
处理后为1->2->5
- 优秀思路1:两次遍历,第一次遍历找出所有的重复值,再遍历一次删除为重复值的链表元素,利用
HashSet
进行存储速度较快。 - 优秀思路2:相邻两位进行比较,为防止开始的2位相同,对链表前面补一个0。(还有一种思路是)
public class Solution {
public ListNode deleteDuplication(ListNode pHead){
if (pHead==null || pHead.next==null) return pHead;
ListNode newNode= new ListNode(0);
newNode.next = pHead;
ListNode pre = newNode;
ListNode last = newNode.next;
while(last != null && last.next != null){
if(last.next.val == last.val){
// 找出最后一个相同值
while(last.next != null && last.next.val == last.val){
last = last.next;
}
pre.next = last.next;
last = last.next;
}else{
pre = last;
last = last.next;
}
}
return newNode.next;
}
}
——————《LeectCode》———————
1. 两数相加
- 题目描述:给你两个非空的链表,表示两个非负的整数。它们每位数字都是按照逆序的方式存储的,并且每个节点只能存储一位数字。请你将两个数相加,并以相同形式返回一个表示和的链表。你可以假设除了数字
0
之外,这两个数都不会以0
开头。 - 优秀思路(我的):在长度不一致的时候,对短的链表补0,解决长度不一致的问题
class Solution {
public ListNode addTwoNumbers(ListNode l1, ListNode l2) {
int dec = 0; // 进位
int temp = 0;
ListNode sum = new ListNode(0);
ListNode index = sum;
while(l1 != null && l2 != null){
if(l1.next != null || l2.next != null){
// 在长度不一致的时候,对短的链表补0
if(l1.next == null) l1.next = new ListNode(0);
if(l2.next == null) l2.next = new ListNode(0);
}
temp = dec + l1.val + l2.val;
dec = temp / 10;
index.next = new ListNode(temp % 10);
index = index.next;
l1 = l1.next;
l2 = l2.next;
}
if(dec != 0) index.next = new ListNode(1);
return sum.next;
}
}
2. 合并两个有序链表(同剑指Offer.5)
- 题目描述:将两个升序链表合并为一个新的 升序 链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。
- 优秀思路(我的):利用归并排序的思想
class Solution {
public ListNode mergeTwoLists(ListNode l1, ListNode l2) {
// 可精简部分 1
if(l1 == null) return l2;
if(l2 == null) return l1;
ListNode newNode = new ListNode(0);
ListNode index = newNode;
while(l1 != null && l2 != null){
if(l1.val < l2.val){
index.next = l1;
l1 = l1.next;
}else{
index.next = l2;
l2 = l2.next;
}
index = index.next;
}
// 可精简部分 2
if(l1 != null) index.next = l1;
if(l2 != null) index.next = l2;
return newNode.next;
}
}
- 代码精简
class Solution {
public ListNode mergeTwoLists(ListNode l1, ListNode l2) {
ListNode newNode = new ListNode(0);
ListNode index = newNode;
// 精简1
while(l1 != null && l2 != null){
if(l1.val < l2.val){
index.next = l1;
l1 = l1.next;
}else{
index.next = l2;
l2 = l2.next;
}
index = index.next;
}
// 精简2
index.next = l1 == null? l2 : l1;
return newNode.next;
}
}
🚩3. 反转链表(同剑指Offer.4)
- 题目描述:给你单链表的头节点 head ,请你反转链表,并返回反转后的链表。
- 优秀思路1:【非递归】每2个相邻元素颠倒地址指向即可
class Solution {
public ListNode reverseList(ListNode head) {
if(head == null || head.next == null) return head;
ListNode pre = null;
ListNode next = null;
while(head != null){ // 结束循环后 head 为null,应该输出pre
next = head.next;
head.next = pre;
pre = head;
head = next;
}
return pre;
}
}
- 优秀思路2:【递归】递归将地址指向颠倒即可,需要注意的是当前结点的下一个节点必须指向∅(即消除原来的地址指向)。如果忽略了这一点,链表中可能会产生环。
class Solution {
public ListNode reverseList(ListNode head) {
// 递归停止条件
if(head == null || head.next == null) return head;
ListNode res = reverseList(head.next); // 将迭代结果存在结点res里
head.next.next = head; // 将当前节点后续所有结点部分的地址指向当前节点
head.next = null; // 不能忽略!!当前节点地址指向空
return res;
}
}
🚩4. 合并K个升序链表(合并2个的升级版)
- 题目描述:给你一个链表数组,每个链表都已经按升序排列。请你将所有链表合并到一个升序链表中,返回合并后的链表。
- 我的思路(31%-90%):定义双指针,从两端开始合并,逐步向中间进行
class Solution {
public ListNode mergeKLists(ListNode[] lists) {
if(lists.length == 0) return null;
if(lists.length == 1) return lists[0];
int left = 0;
int right = lists.length - 1;
ListNode newNode = new ListNode(Integer.MIN_VALUE);
while(left <= right){
if(left == right) newNode = mergeTwoLists(newNode,lists[left]);
else newNode = mergeTwoLists(newNode,mergeTwoLists(lists[left],lists[right]));
left++;
right--;
}
return newNode.next;
}
private ListNode mergeTwoLists(ListNode l1, ListNode l2) {
ListNode newNode = new ListNode(0);
ListNode index = newNode;
// 精简1
while(l1 != null && l2 != null){
if(l1.val < l2.val){
index.next = l1;
l1 = l1.next;
}else{
index.next = l2;
l2 = l2.next;
}
index = index.next;
}
// 精简2
index.next = l1 == null? l2 : l1;
return newNode.next;
}
}
- 优秀思路1:迭代分治合并,即1分2,2分4…,分别两两合并后,再逐步两两合并
class Solution {
public ListNode mergeKLists(ListNode[] lists) {
if(lists.length == 0) return null;
if(lists.length == 1) return lists[0];
if(lists.length == 2) return mergeTwoLists(lists[0], lists[1]);
ListNode newNode = mergeAll(lists,0,lists.length-1);
return newNode;
}
// 分治
private ListNode mergeAll(ListNode[] lists,int left,int right){
if(left==right) return lists[left]; // 不能再分了
int mid = left + (right - left)/2;
ListNode part1 = mergeAll(lists,left,mid);
ListNode part2 = mergeAll(lists,mid+1,right); // 注意要加一
return mergeTwoLists(part1, part2);
}
private ListNode mergeTwoLists(ListNode l1, ListNode l2){
ListNode newNode = new ListNode(0);
ListNode index = newNode;
while(l1 != null && l2 != null){
if(l1.val < l2.val){
index.next = l1;
l1 = l1.next;
}else{
index.next = l2;
l2 = l2.next;
}
index = index.next;
}
index.next = l1 == null? l2 : l1;
return newNode.next;
}
}
- 优秀思路2:利用优先级队列PriorityQueue自动排序的特性,将所有链表加入PriorityQueue,依次找出最小头节点后加入该节点所在链表的其他元素。注意参数的写法很重要,表明是比较两个节点的val值
class Solution {
public ListNode mergeKLists(ListNode[] lists) {
if(lists.length == 0) return null;
if(lists.length == 1) return lists[0];
// 新建一个优先级队列(小根堆)
Queue<ListNode> pq = new PriorityQueue<>((v1, v2) -> v1.val - v2.val); // 参数的写法很重要,表明是比较两个节点的val值
// 所有链表的头结点加入PriorityQueue,此时排序根据的是头节点的值
for (ListNode node: lists) {
if (node != null) pq.offer(node); // offer 添加一个元素并返回true,如果队列已满,则返回false
}
ListNode resNode = new ListNode(0);
ListNode index = resNode;
while (!pq.isEmpty()) {
// 小根堆的根元素最小
ListNode minNode = pq.poll(); // poll移除并返问队列头部的元素,如果队列为空,则返回`null`
index.next = minNode;
// 如果最小节点所在链表还有其他元素,将下一节点加进优先级队列
if (minNode.next != null) {
pq.offer(minNode.next); // offer 添加一个元素并返回true,如果队列已满,则返回false
}
index = minNode;
}
return resNode.next;
}
}
---
5. 删除链表中的节点(脑筋急转弯题目:无返回函数且未给需要操作的对象参数)
- 题目描述:请编写一个函数,使其可以删除某个链表中给定的(非末尾)节点。传入函数的唯一参数为 要被删除的节点 。
- 优秀思路:因为我们访问不了需被删除节点
node
之前的节点p1
,所以无法让p1
指向node
后面的节点p2
,所以采用的方法是用p2
来替换node
。 (这里要注意,传入的需删除节点是链表中真实存在的节点,它里面是存有值和下一个节点的地址的,不能当作一个单纯的外界参数)
class Solution {
public void deleteNode(ListNode node) {
node.val = node.next.val;
node.next = node.next.next;
}
}
6. 排序链表(思路待看)
- 题目描述:给你链表的头结点 head ,请将其按 升序 排列并返回排序后的链表 。
- 我的思路(17%-10%):取出链表的所有值排序,再根据排序值生成新的链表
import java.util.*;
class Solution {
public ListNode sortList(ListNode head) {
if(head == null) return null;
ArrayList<Integer> queue = new ArrayList<Integer>();
// 取出链表值
while(head != null){
queue.add(head.val);
head = head.next;
}
Collections.sort(queue);
// 生成新链表
ListNode res = new ListNode(-5);
ListNode index = res;
for(int i = 0;i<queue.size();i++){
index.next = new ListNode(queue.get(i));
index = index.next;
}
return res.next;
}
}
-
分析:时间复杂度是
O(nlogn)
的排序算法包括归并排序、堆排序和快速排序(快速排序的最差时间复杂度是O(n^2)
),其中最适合链表的排序算法是归并排序。归并排序基于分治算法。最容易想到的实现方式是自顶向下的递归实现,考虑到递归调用的栈空间,自顶向下归并排序的空间复杂度是O(logn)
。如果要达到O(1)
的空间复杂度,则需要使用自底向上的实现方式。 -
优秀思路1:【自顶向下归并排序】,对链表自顶向下归并排序的过程如下。
- 1、找到链表的中点,以中点为分界,将链表拆分成两个子链表。寻找链表的中点可以使用快慢指针的做法,快指针每次移动 2步,慢指针每次移动 1步,当快指针到达链表末尾时,慢指针指向的链表节点即为链表的中点。(聪明!!!)
- 2、对两个子链表分别排序。
- 3、将两个排序后的子链表合并,得到完整的排序后的链表。
- 上述过程可以通过递归实现。递归的终止条件是链表的节点个数小于或等于 1,即当链表为空或者链表只包含 1个节点时,不需要对链表进行拆分和排序。
class Solution {
public ListNode sortList(ListNode head) {
return mergeSort(head);
}
// 归并排序
private ListNode mergeSort(ListNode head){
// 如果没有结点/只有一个结点,无需排序,直接返回
if (head == null || head.next == null) return head;
// 快慢指针找出中位点
ListNode slow = head,fast = head.next.next,l,r; // 此处fast不能设置为head,否则无法处理只有2个元素的情况
while (fast != null && fast.next != null){
slow = slow.next;
fast = fast.next.next;
}
r = mergeSort(slow.next);// 对右半部分进行归并排序
// 链表判断结束的标志:末尾节点.next==null
slow.next = null; // 相当于将head的右半部分截断掉
l = mergeSort(head);// 对左半部分进行归并排序
return mergeList(l,r);
}
// 合并链表
private ListNode mergeList(ListNode l,ListNode r){
ListNode tmpHead=new ListNode(-1);
ListNode p = tmpHead;
while (l!=null&&r!=null){
if (l.val<r.val){
p.next=l;
l=l.next;
}else {
p.next=r;
r=r.next;
}
p=p.next;
}
p.next=l==null?r:l;
return tmpHead.next;
}
}
- 优秀思路2(待看):【自底向上归并排序】由于要求空间复杂度为O(1),所以只能采用自底向上的方式。先两个两个的
merge
,完成一趟后,再 4 个4个的merge
,直到结束。举个简单的例子:`[4,3,1,7,8,9,2,11,5,6]
step=1: (3->4)->(1->7)->(8->9)->(2->11)->(5->6)
step=2: (1->3->4->7)->(2->8->9->11)->(5->6)
step=3: (1->2->3->4->7->8->9->11)->(5->6)
step=4: (1->2->3->4->5->6->7->8->9->11)
class Solution {
public ListNode sortList(ListNode head) {
if (head == null || head.next == null) return head;
int length = 0;
ListNode node = head;
// 求链表长度
while (node != null) {
length++;
node = node.next;
}
ListNode resNode = new ListNode(0, head);
// subLength <<= 1 相当于 subLength = subLength << 1 = subLength^(2)
for (int subLength = 1; subLength < length; subLength <<= 1) {
ListNode prev = resNode, curr = resNode.next;
while (curr != null) {
ListNode head1 = curr;
for (int i = 1; i < subLength && curr.next != null; i++) curr = curr.next;
ListNode head2 = curr.next; // 后部分
curr.next = null;
curr = head2;
for (int i = 1; i < subLength && curr != null && curr.next != null; i++) curr = curr.next;
ListNode next = null;
if (curr != null) {
next = curr.next;
curr.next = null;
}
ListNode merged = merge(head1, head2);
prev.next = merged;
while (prev.next != null) prev = prev.next;
curr = next;
}
}
return resNode.next;
}
public ListNode merge(ListNode head1, ListNode head2) {
ListNode resNode = new ListNode(0);
ListNode temp = resNode, temp1 = head1, temp2 = head2;
while (temp1 != null && temp2 != null) {
if (temp1.val <= temp2.val) {
temp.next = temp1;
temp1 = temp1.next;
} else {
temp.next = temp2;
temp2 = temp2.next;
}
temp = temp.next;
}
if (temp1 != null) {
temp.next = temp1;
} else if (temp2 != null) {
temp.next = temp2;
}
return resNode.next;
}
}
🚩7. 复制带随机指针的链表(同剑指Offer.7)
- 题目描述:给你一个长度为 n 的链表,每个节点包含一个额外增加的随机指针 random ,该指针可以指向链表中的任何节点或空节点。
- 优秀思路1:利用 HashMap 存储新链表和给出链表的关系(两次遍历),从而避免新链表指向原链表
class Solution {
public Node copyRandomList(Node head) {
HashMap<Node,Node> map = new HashMap<Node,Node>();
Node p = head;
while(p != null){
Node newNode = new Node(p.val);
map.put(p,newNode);
p = p.next;
}
p = head;
while(p != null){
Node temp = map.get(p);
temp.next = p.next==null? null : map.get(p.next); // 这样获取出来的next是不指向head的
temp.random = p.random==null? null : map.get(p.random);
p = p.next;
}
return map.get(head); //新的头结点
}
}
- 优秀思路2:递归实现以上过程,强无敌
class Solution {
private HashMap<Node, Node> map = new HashMap<>();
public Node copyRandomList(Node head) {
if (head == null) return null;
// 不管是next还是random指向的节点,只要没有该节点,就创造该节点对应的新节点
if (map.containsKey(head)) return map.get(head);
Node root = new Node(head.val);
map.put(head, root);
root.next = copyRandomList(head.next);
root.random = copyRandomList(head.random);
return root;
}
}
8. 环形链表
- 题目描述:给定一个链表,判断链表中是否有环。
- 普通思路:哈希集合存储已有值进行对比
public class Solution {
public boolean hasCycle(ListNode head) {
if(head == null || head.next == null) return false;
HashSet<ListNode> set = new HashSet<>();
while(head != null){
// 可精简
if(set.contains(head)) return true;
else set.add(head);
head = head.next;
}
return false;
}
}
public class Solution {
public boolean hasCycle(ListNode head) {
Set<ListNode> seen = new HashSet<ListNode>();
while (head != null) {
// 精简后:HashSet的add方法:如果不存在该元素则添加进去并返回true,否则返回false
if (!seen.add(head)) return true;
head = head.next;
}
return false;
}
}
- 优秀思路:快慢指针
9. 两两交换链表中的节点
- 题目描述:给定一个链表,两两交换(每两个交换依次)其中相邻的节点,并返回交换后的链表。
- 优秀思路1:【递归】其中我们应该关心的主要有三点:① 返回值;② 调用单元做了什么;③ 终止条件,在本题中:
- 返回值:交换完成的子链表
- 调用单元:设需要交换的两个点为 head 和 next,head 连接后面交换完成的子链表,next 连接 head,完成交换
- 终止条件:head 为空指针或者 next 为空指针,也就是当前无节点或者只有一个节点,无法进行交换
class Solution {
public ListNode swapPairs(ListNode head) {
if(head == null || head.next == null) return head;
ListNode newNode = head.next;
head.next = swapPairs(newNode.next); // 1指向后面部分
newNode.next = head;// 2指向1
return newNode;
}
}
- 优秀思路1:【迭代】注意要在head前面补一个0,理由是简单分析一下就可以知道调转中间的两个节点时,需要知道三个地址,所以为了方便操作前两位,给head前面补0
class Solution {
public ListNode swapPairs(ListNode head) {
// 简单分析一下就可以知道调转中间的两个节点时,需要操作三次,所以为了方便操作前两位,给head前面补0
ListNode newNode = new ListNode(0,head);
ListNode cur = newNode;
while(cur.next != null && cur.next.next != null){
ListNode start = cur.next;
ListNode end = cur.next.next;
start.next = end.next;
end.next = start;
cur.next = end;
cur = start;
}
return newNode.next;
}
}
10. 回文链表
- 题目描述:请判断一个链表是否为回文链表。
- 我的思路(5% - 5%):利用栈后进先出的特点,根据链表节点数的奇偶性判断。若遇到相同元素则pop,否则push新元素
class Solution {
public boolean isPalindrome(ListNode head) {
if(head == null || head.next == null) return true;
// 求长度
int len = 0;
ListNode index= head;
while(index != null){
len++;
index = index.next;
}
// 判断是否回文
Stack<Integer> stack = new Stack<Integer>();
stack.push(-1);
index = head; // 回到开头,别忘了
if(len % 2 == 0){ // 偶数节点链表
while(index != null){
if(index.val == stack.peek()) stack.pop();
else stack.push(index.val);
index = index.next; //总是忘
}
}else{ // 奇数节点链表
int count = 0;
while(index != null){
count++;
if(count != (len+1)/2){
if(index.val == stack.peek()) stack.pop();
else stack.push(index.val);
}
index = index.next; //总是忘
}
}
if(stack.peek() == -1) return true;
return false;
}
}
- 稍优秀思路(30%-30%):新建存储数组列表存储链表值,再定义双指针从两端开始往中间比较
class Solution {
public boolean isPalindrome(ListNode head) {
List<Integer> vals = new ArrayList<Integer>();
// 将链表的值复制到数组中
ListNode currentNode = head;
while (currentNode != null) {
vals.add(currentNode.val);
currentNode = currentNode.next;
}
// 使用双指针判断是否回文
int front = 0;
int back = vals.size() - 1;
while (front < back) {
if (!vals.get(front).equals(vals.get(back))) {
return false;
}
front++;
back--;
}
return true;
}
}
- 优秀思路(43 % - 57 %):使用快慢指针法找到链表中点,将前半部分(或后半部分)翻转,于另一部分进行比较(注意要忽略奇数链表的中间节点),相同则回文
// 我的代码
class Solution {
public boolean isPalindrome(ListNode head) {
if(head == null || head.next == null) return true;
ListNode fast = head, slow = head, pre = null , post = null;; // pre 保存 slow的上一节点
while(fast != null && fast.next != null){
pre = slow;
fast = fast.next.next;
slow = slow.next;
}
post = pre.next; // 后半部分
pre.next = null; // 截断head的右半部分
if(fast != null){ //奇数项链表,需忽略中点
post = post.next;
}
pre = reverseListNode(head); // 前半部分颠倒
return isSame(pre,post);
}
// 反转链表
private ListNode reverseListNode(ListNode head){
if(head == null || head.next == null) return head;
ListNode pre = null,next = null;
while(head != null){
next = head.next;
head.next = pre;
pre = head;
head = next;
}
return pre;
}
// 比较链表是否相同
private boolean isSame(ListNode node1,ListNode node2){
while(node1 != null && node2 != null){
if(node1.val != node2.val) return false;
node1 = node1.next;
node2 = node2.next;
}
if(node1 != null || node2 != null) return false; // 长度不一致
return true;
}
}
- 优秀思路改进(85% - 54 %):没有必要全部找出前半部分之后再翻转,可以想办法在寻找前半部分的同时进行翻转
class Solution {
public boolean isPalindrome(ListNode head) {
if(head == null || head.next == null) return true;
ListNode fast = head, slow = head, pre = null , preReverse = null;; // pre 保存 slow的上一节点
// 边寻找边反转
while(fast != null && fast.next != null){
// 寻找中点部分
pre = slow;
fast = fast.next.next;
slow = slow.next;
// 反转部分
pre.next = preReverse;
preReverse = pre;
}
if(fast != null){ //奇数项链表,后半部分需忽略中点
slow = slow.next;
}
while(preReverse != null && slow != null){
if(preReverse.val != slow.val) return false;
preReverse = preReverse.next;
slow = slow.next;
}
if(preReverse != null || slow != null) return false; // 两链表长度不一致
return true;
}
}