链表总结
- 对于链表问题,返回结果为头结点时,通常需要先初始化一个预先指针 pre,该指针的下一个节点指向真正的头结点 head。使用预先指针的目的在于链表初始化时无可用节点值,而且链表构造过程需要指针移动,进而会导致头指针丢失,无法返回结果。
ListNode pre = new ListNode(0);
ListNode cur = pre;
先分割再合并:
- 23.合并K个升序链表
- 148.排序链表(归并排序)
双指针
找中间节点
109. 有序链表转换成二叉搜索树
这个单链表是按升序排序的,我们只需找到他的中间节点,让他成为树的根节点,中间节点前面的就是根节点左子树的所有节点,中间节点后面的就是根节点右子树的所有节点。然后递归分别对左右子树进行相同操作
class Solution {
public TreeNode sortedListToBST(ListNode head) {
// write code here
if(head==null){
return null;
}
if(head.next==null){
return new TreeNode(head.val);
}
//找到中间节点
ListNode fast = head;
ListNode slow = head;
ListNode prev = null;
while(fast!=null && fast.next!=null){
prev = slow;
fast = fast.next.next;
slow = slow.next;
}
//将链表断开为两部分
prev.next = null;
TreeNode node = new TreeNode(slow.val);
node.left = sortedListToBST(head);
node.right = sortedListToBST(slow.next);
return node;
}
}
通过快慢指针解决不知道链表总长度的问题
由于不知道链表总长度,但又希望通过一次遍历解决问题,所以需要通过两个指针找到等式来解决问题
找链表的中间结点(快慢指针)
问题的关键在于我们无法直接得到单链表的长度 n,常规方法也是先遍历链表计算 n,再遍历一次得到第 n / 2 个节点,也就是中间节点。
如果想一次遍历就得到中间节点,使用「快慢指针」的技巧:
我们让两个指针 slow 和 fast 分别指向链表头结点 head。
每当慢指针 slow 前进一步,快指针 fast 就前进两步,这样,当 fast 走到链表末尾时,slow 就指向了链表中点。
class Solution {
public ListNode middleNode(ListNode head) {
ListNode fast = head;
ListNode low = head;
while(fast!=null && fast.next!=null){
fast = fast.next.next;
low = low.next;
}
return low;
}
}
获取倒数第k个元素
如果找正数第k个元素则直接一次遍历到k即可,如果知道链表的个数n,倒数第k个元素就是n-k+1,但我们不知道链表的个数n
但如果用两个指针,第一个指针p先走到k的位置,则他剩余的节点是n-k,此时我们让指向head的另一个指针q和p一起移动,当p指向null时q指向的位置就是n-k+1的节点,即倒数第 k 个节点
class Solution {
// 返回链表的倒数第 k 个节点
ListNode findFromEnd(ListNode head, int k) {
ListNode p1 = head;
// p1 先走 k 步
for (int i = 0; i < k; i++) {
p1 = p1.next;
}
ListNode p2 = head;
// p1 和 p2 同时走 n - k 步
while (p1 != null) {
p2 = p2.next;
p1 = p1.next;
}
// p2 现在指向第 n - k + 1 个节点,即倒数第 k 个节点
return p2;
}
}
19. 删除链表的倒数第 N 个结点
- 先找到倒数第N+1的节点
- 删除倒数第N个节点
- 需要用的dummy节点
因为如果要删的是第一个节点,我们找N+1的时候就空指针了
public ListNode removeNthFromEnd(ListNode head, int n) {
// 如果要删的是倒数第n个节点,即第一个节点,那我们要找的是倒数第n+1个节点,如果不用dummy就会空指针
ListNode dummy = new ListNode(-1);
dummy.next = head;
ListNode x = findNthFromEnd(dummy, n+1);
x.next = x.next.next;
return dummy.next;
}
public ListNode findNthFromEnd(ListNode head, int n){
ListNode p = head;
ListNode q = head;
for (int i=0;i<n;i++){
p = p.next;
}
while (p!=null){
p = p.next;
q = q.next;
}
return q;
}
环形链表
判断链表是否有环
- 判断链表是否有环
当链表有环时,快慢指针陷入环中,快指针总会追上慢指针。
若不存在环,那么快指针一定会先走到链表尾部。
public class Solution {
public boolean hasCycle(ListNode head) {
ListNode fast = head;
ListNode low = head;
while(fast!=null && fast.next!=null){
fast = fast.next.next;
low = low.next;
if(fast==low){ //快的追上慢的了 说明有环
return true;
}
}
return false;
}
}
返回链表开始入环的第一个节点
- 返回链表开始入环的第一个节点。如果链表无环,则返回null。
用快慢指针:
- 第一步:快指针走两步慢指针走一步找到相遇点
- 第二步:head和相遇点同时向后走,相遇点即入环节点
public class Solution {
public ListNode detectCycle(ListNode head) {
ListNode fast = head;
ListNode slow = head;
boolean hasCycle = false;
// 判断是否有环:找到第一次相遇点
while(fast!=null && fast.next!=null){
fast = fast.next.next;
slow = slow.next;
if(fast==slow){
hasCycle = true;
break;
}
}
if(hasCycle){
slow = head;
//head 和 相遇点 一起往后走 相遇点即为入环点
while(fast!=slow){
fast = fast.next;
slow = slow.next;
}
return fast;
}
else{
return null;
}
}
}
判断环的长度
- 如果存在环,如何判断环的长度
快慢指针
- 第一步:快慢指针相遇
- 第二步:相遇后继续走 直到第二次相遇,两次相遇间的移动次数即为环的长度
反转链表
反转整个链表
方法一:迭代
设置prev为null,curr为head,当curr不为null时:
- tmp = curr.next
- curr.next=prev
- prev = curr
- curr = tmp
class Solution {
public ListNode reverseList(ListNode head) {
ListNode prev = null;
ListNode curr = head;
while(curr!=null){
ListNode tmp = curr.next;
curr.next = prev;
prev = curr;
curr = tmp;
}
return prev;
}
}
方法二:递归
class Solution {
public ListNode reverseList(ListNode head) {
//base case
if(head==null || head.next==null){ //只有当前结点一个结点则返回
return head;
}
ListNode last = reverseList(head.next); //得到反转后的头结点
head.next.next = head;
head.next=null;
return last;
}
}
反转链表前N个节点
ListNode successor = null; // 后驱节点
// 反转以 head 为起点的 n 个节点,返回新的头结点
ListNode reverseN(ListNode head, int n) {
if (n == 1) {
// 记录第 n + 1 个节点
successor = head.next;
return head;
}
// 以 head.next 为起点,需要反转前 n - 1 个节点
ListNode last = reverseN(head.next, n - 1);
head.next.next = head;
// 让反转之后的 head 节点和后面的节点连起来
head.next = successor;
return last;
}
反转单链表的一部分
方法一:迭代
- 先用一个for循环找到第m个位置
prev 和curr一直向后迭代left-1次,这时的curr为反转链表的起始位置,prev应该记录为之后连接的起始结点 - 将m和n之间的元素反转
反转后的prev为反转链表的起始结点,此节点应该为之前prev节点的下一个结点(如果之前节点为空则head为prev)
反转后的curr为反转链表的结束结点的下一个结点,此节点应该为之前curr结点的下一个结点 - 将反转后的和原来的连接起来
class Solution {
public ListNode reverseBetween(ListNode head, int left, int right) {
ListNode prev = null;
ListNode cur = head;
// 1、先找到left cur指向left
for(int i=1;i<left;i++){
prev = cur;
cur = cur.next;
}
// 反转链表的前一个节点,用来连接反转后的链表
ListNode firstNode = prev;
// 反转后的最后一个节点,用来连接 反转链表的后一个节点
ListNode lastNode = cur;
// 2、开始反转left到right
while (left<=right && cur!=null){
ListNode tmp = cur.next;
cur.next = prev;
prev = cur;
cur = tmp;
left++;
}
// 3、连接
if (firstNode!=null){
// 反转的前部分有节点
firstNode.next = prev;
}else{
// 反转的前部分无节点
head = prev;
}
lastNode.next = cur;
return head;
}
}
带dummy节点的迭代
public ListNode reverseBetween(ListNode head, int left, int right) {
ListNode dummyHead = new ListNode(0);
dummyHead.next = head;
ListNode cur = head;
ListNode prev = dummyHead;
// 找到要反转的第一个节点
for (int i = 0; i < left-1; i++) {
cur = cur.next;
prev = prev.next;
}
ListNode newHead = reverseN(cur, right-left+1);
prev.next = newHead;
return dummyHead.next;
}
public ListNode reverseN(ListNode head, int n){
ListNode oldHead = head;
ListNode prev = null;
ListNode cur = head;
for (int i = 0; i < n; i++) {
ListNode tmp = cur.next;
cur.next = prev;
prev = cur;
cur = tmp;
}
// 连接
oldHead.next = cur;
return prev;
}
递归
- 一直head.next 前进到left减到1
- 就可以用reverseN 反转前n个链表
class Solution {
ListNode successor = null;//后驱节点
public ListNode reverseBetween(ListNode head, int left, int right) {
if(left==1){
return reverseN(head,right);
}
// 前进到反转的起点 触发base case
head.next = reverseBetween(head.next,left-1,right-1);
return head;
}
public ListNode reverseN(ListNode head,int n){
if(n==1){
successor = head.next;
return head;
}
// 以head.next为起点,需要反转前n-1个节点
ListNode last = reverseN(head.next,n-1);
head.next.next = head;
//让反转之后的head节点和后面的节点连起来
head.next = successor;
return last;
}
}
25. K 个一组翻转链表
- 先看剩余部分长度如果小于k则不用翻转直接返回
- 找到要翻转的部分 head tail,进行翻转 注意翻转的条件是prev!=tail
- 将翻转后的head和tail 和原链表拼接
class Solution {
public ListNode reverseKGroup (ListNode head, int k) {
// write code here
ListNode dummyHead = new ListNode(0);
dummyHead.next = head;
ListNode pre = dummyHead;
while(head!=null){
ListNode tail = pre;
//查看剩余部分长度是否大于等于k ,小于k的话直接保持原样
for(int i = 0;i<k;i++){
tail=tail.next;
if(tail==null){
return dummyHead.next;
}
}
// 从pre 到tail 进行翻转
ListNode nex = tail.next;
ListNode[] reverse = myReverse(head,tail);
head = reverse[0];
tail = reverse[1];
// 把子链表接回原链表
pre.next = head;
tail.next = nex;
// 继续进行接下来的翻转
pre = tail;
head = tail.next;
}
return dummyHead.next;
}
public ListNode[] myReverse(ListNode head,ListNode tail){
ListNode prev = null;
ListNode curr = head;
while(prev!=tail){// 结束条件
ListNode tmp = curr.next;
curr.next = prev;
prev = curr;
curr = tmp;
}
return new ListNode[]{tail,head};
}
}
回文链表
- 第一步:通过快慢指针得到链表的中间节点
- 通过双指针将后一段链表反转
- 判断前半部分链表和后半部分链表是否相等
class Solution {
public boolean isPalindrome(ListNode head) {
ListNode fast=head;
ListNode slow=head;
//通过快慢指针得到链表的中间节点
while(fast!=null && fast.next!=null){
slow = slow.next;
fast = fast.next.next;
}
//将后一段链表反转
ListNode prev = null;
ListNode curr = slow;
while(curr!=null){
ListNode tmp = curr.next;//存下一个节点
curr.next = prev;//当前结点的下一个节点是前一个结点
prev = curr;
curr = tmp;
}
//判断两个链表是否相等
while(prev!=null){
if(head.val!=prev.val){
return false;
}
head = head.next;
prev = prev.next;
}
return true;
}
}
重排链表
方法一: 找到中间结点+后半段逆序+合并链表
class Solution {
public void reorderList(ListNode head) {
if(head==null){
return ;
}
// 找到中间节点
ListNode fast = head;
ListNode slow = head;
while(fast!=null && fast.next!=null){
fast = fast.next.next;
slow = slow.next;
}
ListNode l1 = head;
ListNode l2 = slow.next;
slow.next= null; //要断开
//后半部分链表反转
ListNode prev = null;
ListNode curr = l2;
while(curr!=null){
ListNode tmp = curr.next;
curr.next = prev;
prev = curr;
curr = tmp;
}
// 合并链表
mergeList(l1,prev);
}
public void mergeList(ListNode l1, ListNode l2){
ListNode l1_tmp;
ListNode l2_tmp;
while(l1!=null && l2!=null){
l1_tmp = l1.next;
l2_tmp = l2.next;
l1.next = l2;
l1 = l1_tmp;
l2.next = l1;
l2 = l2_tmp;
}
}
}
方法二: 将原始链表放到数组中
首先将原始链表的每一个节点存放在一个数组中,然后我们取首尾指针向中间遍历,每次循环我们需要将左指针的节点连上右指针的节点,在节点连上之后,我们需要将右指针连上未排序的首节点。
class Solution {
public void reorderList(ListNode head) {
if (head == null) {
return;
}
List<ListNode> list = new ArrayList<ListNode>();
ListNode node = head;
while (node != null) {
list.add(node);
node = node.next;
}
int i = 0, j = list.size() - 1;
while (i < j) {
list.get(i).next = list.get(j);
i++;
if (i == j) {
break;
}
list.get(j).next = list.get(i);
j--;
}
list.get(i).next = null;
}
}
相交链表
用la和lb两个指针 走两轮
- 第一轮:抹除长度差 当走到了尾部,则指向另一个的头节点
- 第二轮:两个同时向后走,就走到相交处
public class Solution {
public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
ListNode la = headA;
ListNode lb = headB;
while(la!=lb){//la 走到了尾部,则就指向headB 开始第二轮
if(la==null){
la = headB;
}
else{
la = la.next;
}
if(lb==null){
lb = headA;
}
else{
lb = lb.next;
}
}
return la;
}
}
链表排序问题
147. 对链表进行插入排序
插入排序的算法复杂度是O(n2)
思路:
- 用lastSorted指向已排序部分的最后一个节点
- curr 指向待插入元素,和lastSorted进行比较
- curr的值大于等于lastSorted 则lastSorted直接后移
- 小于 要从头遍历找到 curr插入的地方,即 prev.next.val>curr.val的地方
class Solution {
public ListNode insertionSortList(ListNode head) {
if(head==null){
return head;
}
// write code here
ListNode dummyHead = new ListNode(0);
dummyHead.next = head;
// 已排序部分的最后一个节点
ListNode lastSorted = head;
//待插入的元素
ListNode curr = head.next;
while(curr!=null){
if(curr.val>=lastSorted.val){
//lastSorted = lastSorted.next;
lastSorted = curr;
}
else{
ListNode prev = dummyHead;
//prev向后移,直到找到curr的插入位置
while(prev.next.val<=curr.val){
prev = prev.next;
}
lastSorted.next = curr.next;
//将curr插入
curr.next = prev.next;
prev.next = curr;
}
curr = lastSorted.next;
}
return dummyHead.next;
}
}
148. 排序链表(归并排序)
题目的进阶问题要求达到O(nlogn) 的时间复杂度和O(1) 的空间复杂度,时间复杂度是 O(nlogn) 的排序算法包括归并排序、堆排序和快速排序(快速排序的最差时间复杂度是 O(n^2),其中最适合链表的排序算法是归并排序。
递归进行如下操作:
- 找到链表的中点,以中点为分界,将链表拆成两个子链表。
- 拆成的子链表进行排序
- 将排序后的子链表进行合并
递归的终止条件:链表的节点个数小于或等于1
自顶向下归并排序:
时间复杂度:O(nlogn)
空间复杂度:O(logn),递归调用的栈空间
public ListNode sortList (ListNode head) {
// write code here
return sortList(head,null);
}
public ListNode sortList(ListNode head,ListNode tail){
// 递归终止条件
if(head==null){
return head;
}
if(head.next==tail){
head.next = null;
return head;
}
// 找到中间节点
ListNode fast = head;
ListNode slow = head;
while(fast!=tail){
fast = fast.next;
slow = slow.next;
if(fast!=tail){
fast = fast.next;
}
}
ListNode mid = slow;
ListNode l1 = sortList(head,mid);
ListNode l2 = sortList(mid,tail);
//合并
ListNode sorted = merge(l1,l2);
return sorted;
}
public ListNode merge(ListNode head1,ListNode head2){
ListNode head = new ListNode(0);
ListNode l = head;
while(head1!=null && head2!=null){
if(head1.val<=head2.val){
l.next = head1;
head1 = head1.next;
}
else{
l.next = head2;
head2 = head2.next;
}
l = l.next;
}
l.next = head1==null?head2:head1;
return head.next;
}
链表经典题目
2.两数相加
给出两个 非空 的链表用来表示两个非负的整数。其中,它们各自的位数是按照 逆序 的方式存储的,并且它们的每个节点只能存储 一位 数字。
如果,我们将这两个数相加起来,则会返回一个新的链表来表示它们的和。
您可以假设除了数字 0 之外,这两个数都不会以 0 开头。
示例:
输入:(2 -> 4 -> 3) + (5 -> 6 -> 4)
输出:7 -> 0 -> 8
原因:342 + 465 = 807
class Solution {
public:
ListNode* addTwoNumbers(ListNode* l1, ListNode* l2) {
ListNode* l=new ListNode;
ListNode* head=new ListNode;
l=head;
int carry=0;
int sum;
while(l1!=NULL||l2!=NULL){
int l1val = l1!=NULL?l1->val:0;
int l2val = l2!=NULL?l2->val:0;
sum = l1val+l2val+carry;//取l1的值和l2的值相加
carry = sum/10;//进位
l->next = new ListNode(sum%10);
l=l->next;
if(l1!=NULL){l1=l1->next;}
if(l2!=NULL){l2=l2->next;}
}
if(carry){ //最后一位有进位
l->next = new ListNode(carry);
l=l->next;
}
return head->next;
}
};
有序链表
合并两个有序链表
将两个升序链表合并为一个新的 升序 链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。
输入:l1 = [1,2,4], l2 = [1,3,4]
输出:[1,1,2,3,4,4]
class Solution {
public ListNode mergeTwoLists(ListNode l1, ListNode l2) {
ListNode head = new ListNode();
ListNode l = head;
while(l1!=null && l2!=null){
if(l1.val<=l2.val){
l.next = l1;
l1 = l1.next;
}
else{
l.next = l2;
l2 = l2.next;
}
l = l.next;
}
// 合并后 l1 和 l2 最多只有一个还未被合并完,我们直接将链表末尾指向未合并完的链表即可
l.next = l1==null?l2:l1;
return head.next;
}
}
23. 合并k个升序链表(分治)
给你一个链表数组,每个链表都已经按升序排列。
请你将所有链表合并到一个升序链表中,返回合并后的链表。
示例 1:
输入:lists = [[1,4,5],[1,3,4],[2,6]]
输出:[1,1,2,3,4,4,5,6]
解释:链表数组如下:
[
1->4->5,
1->3->4,
2->6
]
将它们合并到一个有序链表中得到。
1->1->2->3->4->4->5->6
思路:
通过二分法把k个链表分割,然后再两两进行合并
class Solution {
public ListNode mergeKLists(ListNode[] lists) {
return merge(lists,0,lists.length-1);
}
public ListNode merge(ListNode[] lists,int l,int r){
if(l==r){
return lists[l];
}
if(l>r){
return null;
}
int mid = (l+r)>>1;
return mergeTwoLists(merge(lists,l,mid),merge(lists,mid+1,r));
}
public ListNode mergeTwoLists(ListNode l1,ListNode l2){
ListNode head = new ListNode(0);
ListNode l = head;
while(l1!=null && l2!=null){
if(l1.val<=l2.val){
l.next = l1;
l1 = l1.next;
}
else{
l.next = l2;
l2 = l2.next;
}
l = l.next;
}
l.next = l1==null?l2:l1;
return head.next;
}
}
public ListNode mergeKLists(ListNode[] lists) {
ListNode dummy = new ListNode(-1);
ListNode p = dummy;
// 优先级队列:最小堆
PriorityQueue<ListNode> pq = new PriorityQueue<>(lists.length,((a,b)-> (a.val-b.val));
// 1、将k个链表的头节点加入最小堆
for (ListNode head : lists){
pq.add(head);
}
while (p!=null){
// 得到k个节点的最小值
ListNode minNode = pd.poll();
p.next = minNode;
pq.add(minNode.next);
p=p.next;
}
return dummy.next;
}
删除排序链表中的重复元素(哑节点)
给定一个排序链表,删除所有重复的元素,使得每个元素只出现一次。
示例 1:
输入: 1->1->2
输出: 1->2
示例 2:
输入: 1->1->2->3->3
输出: 1->2->3
class Solution {
public ListNode deleteDuplicates(ListNode head) {
ListNode l = head;
while(l!=null && l.next!=null){
if(l.next.val==l.val){ //相等就删除下一个节点
l.next = l.next.next;
}
else{ //直到不相等才指向下一个节点
l = l.next;
}
}
return head;
}
}
给定一个排序链表,删除所有含有重复数字的节点,只保留原始链表中 没有重复出现 的数字。
示例 1:
输入: 1->2->3->3->4->4->5
输出: 1->2->5
示例 2:
输入: 1->1->1->2->3
输出: 2->3
- 需要一个虚拟头节点dummyHead,用prev指向dummyHead,用prev来控制新的链表的连接
这样在删除重复节点后,剩余节点就挂在prev之后 - 当有重复节点,prev.next = difNode prev的下一个节点为和前面不重复的,这样就把之前那些重复的都删了
- 没有重复节点,prev = curr
public class Solution {
/**
*
* @param head ListNode类
* @return ListNode类
*/
public ListNode deleteDuplicates (ListNode head) {
// write code here
ListNode dummyHead = new ListNode(0);
dummyHead.next = head;
ListNode prev = dummyHead;
ListNode curr = prev.next;
boolean hasRepeatNode = false;
while(curr!=null){
hasRepeatNode = false;
ListNode difNode = curr.next;
while(difNode!=null && difNode.val==curr.val){
difNode = difNode.next;
hasRepeatNode = true;
}
if(hasRepeatNode){// 有重复节点
prev.next = difNode;
}
else{
prev = curr;
}
curr = difNode;
}
return dummyHead.next;
}
}
旋转链表
给定一个链表,旋转链表,将链表每个节点向右移动 k 个位置,其中 k 是非负数。
输入: 1->2->3->4->5->NULL, k = 2
输出: 4->5->1->2->3->NULL
解释:
向右旋转 1 步: 5->1->2->3->4->NULL
向右旋转 2 步: 4->5->1->2->3->NULL
解:
- 找到旧链表的尾,将其next指向head,同时获得链表长度len
- 循环len-len%k-1 此得到新链表的尾
- 新链表尾的next即为新链表的头
class Solution {
public ListNode rotateRight(ListNode head, int k) {
if(head==null || head.next ==null){
return head;
}
ListNode new_tail = head; //新链表的尾
ListNode old_tail = head; //旧链表的尾
int len = 1; //链表长度
// 找到旧链表的尾部
while(old_tail.next!=null){
old_tail = old_tail.next;
len++;
}
old_tail.next = head;
// 找到新链表的尾
for(int i = 0;i<len-k%len-1;i++){
new_tail = new_tail.next;
}
head = new_tail.next;
new_tail.next = null;
return head;
}
}
86.划分链表
思路: 用两个哑节点分别管理小于x的链和大于等于x的链
这里要注意理解题目意思是只需要小于等于x的节点位于链表前面即可,不要求小于在前,等于在中,大于在后。
public class Solution {
/**
*
* @param head ListNode类
* @param x int整型
* @return ListNode类
*/
public ListNode partition (ListNode head, int x) {
// write code here
// 哑节点指向两个链表的头部
ListNode l1 = new ListNode(0);
ListNode l2 = new ListNode(0);
// 指针指向两个链表的尾部
ListNode l1_curr = l1;
ListNode l2_curr = l2;
while(head!=null){
if(head.val<x){
l1_curr.next = head;
l1_curr = l1_curr.next;
}
else{
l2_curr.next = head;
l2_curr = l2_curr.next;
}
head = head.next;
}
l1_curr.next = l2.next;
l2_curr.next= null;
return l1.next;
}
}
复制带随机指针的链表( LeetCode 138 )
- 第一次遍历:用hash表存储每个节点,但存储不了每个节点的next和random指针
- 第二次遍历:找到每个节点的next和random指针
//给你一个长度为 n 的链表,每个节点包含一个额外增加的随机指针 random ,该指针可以指向链表中的任何节点或空节点。
//
// 构造这个链表的 深拷贝。 深拷贝应该正好由 n 个 全新 节点组成,其中每个新节点的值都设为其对应的原节点的值。新节点的 next 指针和 random
//指针也都应指向复制链表中的新节点,并使原链表和复制链表中的这些指针能够表示相同的链表状态。复制链表中的指针都不应指向原链表中的节点 。
//
// 例如,如果原链表中有 X 和 Y 两个节点,其中 X.random --> Y 。那么在复制链表中对应的两个节点 x 和 y ,同样有 x.random
//--> y 。
//
// 返回复制链表的头节点。
//
// 用一个由 n 个节点组成的链表来表示输入/输出中的链表。每个节点用一个 [val, random_index] 表示:
//
//
// val:一个表示 Node.val 的整数。
// random_index:随机指针指向的节点索引(范围从 0 到 n-1);如果不指向任何节点,则为 null 。
//
//
// 你的代码 只 接受原链表的头节点 head 作为传入参数。
//
//
//
// 示例 1:
//
//
//
//
//输入:head = [[7,null],[13,0],[11,4],[10,2],[1,0]]
//输出:[[7,null],[13,0],[11,4],[10,2],[1,0]]
//
//
// 示例 2:
//
//
//
//
//输入:head = [[1,1],[2,1]]
//输出:[[1,1],[2,1]]
//
//
// 示例 3:
//
//
//
//
//输入:head = [[3,null],[3,0],[3,null]]
//输出:[[3,null],[3,0],[3,null]]
//
//
//
//
// 提示:
//
//
// 0 <= n <= 1000
//
// -10⁴ <= Node.val <= 10⁴
// Node.random 为 null 或指向链表中的节点。
//
//
//
//
// Related Topics 哈希表 链表 👍 1326 👎 0
class Solution {
public Node copyRandomList(Node head) {
if (head==null){
return null;
}
Node cur = head;
// key是原节点有next和random, value是和原节点一样的新节点,但没有next和random
Map<Node,Node> map = new HashMap<>();
// 第一次遍历存储原节点
while (cur!=null){
// 以原链表的节点为 Key,构建一个 Map
// Map 的 Value 为一个新链表中的节点
// 新节点的值 val 和原链表的值 val 一样
// 但原链表中的每个节点都有 next 和 random 指针,而 Map 中的 Value 没有 next 和 random 指针
map.put(cur, new Node(cur.val));
cur = cur.next;
}
// 第二次遍历 把新节点的next 和random指针都添加上
cur = head;
while (cur!=null){
// 1、找next
Node curNode = map.get(cur);
Node nextNode = map.get(cur.next);
curNode.next = nextNode;
// 2、找random
Node randomNode = map.get(cur.random);
curNode.random = randomNode;
cur=cur.next;
}
return map.get(head);
}
}
//leetcode submit region end(Prohibit modification and deletion)
328.奇偶链表
给定单链表的头节点 head ,将所有索引为奇数的节点和索引为偶数的节点分别组合在一起,然后返回重新排序的列表。
第一个节点的索引被认为是 奇数 , 第二个节点的索引为 偶数 ,以此类推。
请注意,偶数组和奇数组内部的相对顺序应该与输入时保持一致。
你必须在 O(1) 的额外空间复杂度和 O(n) 的时间复杂度下解决这个问题。
示例 1:
输入: head = [1,2,3,4,5]
输出: [1,3,5,2,4]
示例 2:
输入: head = [2,1,3,5,6,4,7]
输出: [2,3,6,7,1,5,4]
class Solution {
public ListNode oddEvenList(ListNode head) {
if (head==null || head.next==null){
return head;
}
// 奇数
ListNode odd = head;
// 偶数
ListNode even = head.next;
// 偶数头结点
ListNode evenHead = even;
while(even!=null && even.next!=null){
odd.next = even.next;
odd = odd.next;
even.next = odd.next;
even = even.next;
}
odd.next = evenHead;
return head;
}
}
146. LRU缓存
请你设计并实现一个满足 LRU (最近最少使用) 缓存 约束的数据结构。
实现 LRUCache 类:
LRUCache(int capacity) 以 正整数 作为容量 capacity 初始化 LRU 缓存
int get(int key) 如果关键字 key 存在于缓存中,则返回关键字的值,否则返回 -1 。
void put(int key, int value) 如果关键字 key 已经存在,则变更其数据值 value ;如果不存在,则向缓存中插入该组 key-value 。如果插入操作导致关键字数量超过 capacity ,则应该 逐出 最久未使用的关键字。
函数 get 和 put 必须以 O(1) 的平均时间复杂度运行
hashMap get 快,但数据无固定顺序;链表插入删除快,但查找慢。所以我们将hashMap和双向链表结合起来,用hash链表LinkedHashMap。
- 如果map存在key
- 删除key 2. 将key value作为最近使用元素插入
- map不存在key
- 看当前链表的size是否已满,如果满了先删除最久未使用节点,也就是链表首节点
- 将key value作为最近使用元素插入
import java.util.HashMap;
class Node {
public int key,val;
public Node prev, next;
public Node(int k, int v){
this.key = k;
this.val = v;
}
}
class DoubleList{
private Node head,tail;
// 链表个数
private int size;
// 初始化链表
public DoubleList(){
head = new Node(0,0);
tail = new Node(0,0);
head.next = tail;
tail.prev = head;
size = 0;
}
// 向尾部插入元素
public void addLast(Node x){
x.prev = tail.prev;
tail.prev.next = x;
x.next = tail;
tail.prev = x;
size++;
}
// 删除链表的x节点
public void remove(Node x){
x.prev.next = x.next;
x.next.prev = x.prev;
size--;
}
// 删除链表中的第一个节点,并返回删除的元素
public Node removeFirst(){
if (head.next==tail){
return null;
}
Node first = head.next;
remove(first);
return first;
}
public int size(){
return size;
}
}
class LRUCache {
// hash表
private HashMap<Integer,Node> map;
// 双向链表
private DoubleList cache;
// 最大容量
private int cap;
public LRUCache(int capacity) {
cap = capacity;
map = new HashMap<>();
cache = new DoubleList();
}
public int get(int key) {
// 不存在 返回-1
if (!map.containsKey(key)){
return -1;
}
Node node = map.get(key);
// 变成最近使用
makeRecently(key);
return node.val;
}
public void put(int key, int value) {
// 1、key 存在
if (map.containsKey(key)){
// 删除旧数据
deleteKey(key);
// 将新数据置为最近使用
addRecently(key,value);
return ;
}
// 2、key不存在
// 当前容量是否已满,满了就删除最久未使用的节点
if (cache.size()==cap){
removeLeastRecently();
}
// 将新数据置为最近使用
addRecently(key,value);
}
// 将某个key提升为最近最久使用
public void makeRecently(int key){
Node x = map.get(key);
// 先从链表中删除这个节点
cache.remove(x);
// 插入到队尾
cache.addLast(x);
}
// 添加最近使用
public void addRecently(int key, int val){
// 插入map
Node x = new Node(key,val);
map.put(key,x);
// 添加到队尾
cache.addLast(x);
}
// 删除key
public void deleteKey(int key){
Node x = map.get(key);
cache.remove(x);
map.remove(key);
}
// 删除最久未使用
public void removeLeastRecently(){
Node node = cache.removeFirst();
map.remove(node.key);
}
}
使用java内置函数 linkedHashMap
class LRUCache {
// 用java 内置函数 linkedHashMap
LinkedHashMap<Integer,Integer> cache = new LinkedHashMap<>();
int cap;
public LRUCache(int capacity) {
this.cap = capacity;
}
public int get(int key) {
if (!cache.containsKey(key)){
return -1;
}
// 把key变为最近使用
makeRecently(key);
return cache.get(key);
}
public void put(int key, int value) {
if (cache.containsKey(key)){
cache.put(key,value);
makeRecently(key);
return ;
}
if (cache.size()>=cap){
// 找到链表的头部
Integer first = cache.keySet().iterator().next();
cache.remove(first);
}
cache.put(key,value);
}
public void makeRecently(int key){
Integer value = cache.get(key);
cache.remove(key);
cache.put(key,value);
}
}
基础操作
插入节点
- 在p的结点后插入元素
ListNode tmp = new ListNode(data); tmp.next = p.next; p.next = tmp;
删除节点
删除单向链表中间的某个节点
/*
实现一种算法,删除单向链表中间的某个节点(即不是第一个或最后一个节点),假定你只能访问该节点。
示例:
输入:单向链表a->b->c->d->e->f中的节点c
结果:不返回任何数据,但该链表变为a->b->d->e->f
*/
// 方法一:自己的方法
class Solution {
public void deleteNode(ListNode node) {
while(node.next!=null){
node.val = node.next.val;
if(node.next.next==null){
node.next=null;
return ;
}
node = node.next;
}
}
}
删除p节点后面的元素
if(p==null || p.next == null){
return ;
}
p.next = p.next.next;
24. 两两交换链表中的节点
给定一个链表,两两交换其中相邻的节点,并返回交换后的链表。
你不能只是单纯的改变节点内部的值,而是需要实际的进行节点交换。
示例:
给定 1->2->3->4, 你应该返回 2->1->4->3.
迭代:
dummy head
temp start end
temp是游标,起初指向dummy节点,每次要交换的节点就是temp.next和temp.next.next
每次交换的时候我们 得记住他前面的节点和后面的节点,所以
- 用temp连接交换后的首节点
temp.next = end - 用交换后的尾结点 连接下一个节点
start.next = end.next - 交换
end.next=start - 移动游标进行下一次交换
temp = start;
class Solution {
public ListNode swapPairs(ListNode head) {
ListNode pre = new ListNode(-1);
pre.next = head;
// 前进的游标
ListNode temp =pre;
while(temp.next!=null && temp.next.next!=null){
// 要交换的两个节点
ListNode start = temp.next;
ListNode end = temp.next.next;
// 交换
// 用temp连接交换后的首节点
temp.next = end;
// 用交换后的尾结点 连接下一个节点
start.next = end.next;
// 交换
end.next = start;
// 移动游标进行下一次交换
temp = start;
}
return pre.next;
}
}
递归:
关注三个:
- 本次递归的终止条件
只剩下一个结点或者没结点了就没得交换了 - 本次递归要做什么
本次要换的是head和head.next,要把本次换后的节点返回给上级即head.next, 要把本次和下个节点返回的连接起来,交换本次! - 要返回给上一个节点什么
返回已经处理好的链表
class Solution {
public ListNode swapPairs(ListNode head) {
// 终止条件
if (head==null || head.next==null){
return head;
}
// 递归:交换的是head 和head.next
ListNode end = head.next;
// 用交换后的尾结点 连接下一个节点
head.next = swapPairs(head.next.next);
// 交换
end.next = head;
return end;
}
}