文章目录
147. 对链表进行插入排序
(https://leetcode-cn.com/problems/insertion-sort-list/)
- 当前节点和前面的节点构成有序:
- 当前节点和前面的节点构成无序,需要移动,从链表头部往后寻找(数组一般是从后往前寻找):
class Solution {
public ListNode insertionSortList(ListNode head) {
if(head==null)
return null;
ListNode dummy=new ListNode(-1);
dummy.next=head;
ListNode lastSorted=head,cur=head.next;
while(cur!=null)
{
if(lastSorted.val<=cur.val)//当前节点和前面的节点已经构成有序
{
lastSorted=lastSorted.next;
}
else
{
ListNode pre=dummy;
while(pre.next.val<=cur.val)
pre=pre.next;//pre指向第一个大于cur的节点的前一个节点
lastSorted.next=cur.next;
cur.next=pre.next;
pre.next=cur;
}
cur=lastSorted.next;//cur指向下个未排序的节点
}
return dummy.next;
}
}
//O(n^2)
//O(1)
2. 两数相加
https://leetcode-cn.com/problems/add-two-numbers/
思路:对链表进行加法操作,从两条链表的头结点开始,将对应的节点值相加并求进位,由于两条链表的长度可能不一样,所以当遍历完某条链表时,可以用0值来代替不存在的节点,方便计算,注意的一点时,如果最后的进位不为0,说明最高位有进位,需要额外添加1个值为1的节点,比如1+9 返回01
class Solution {
public ListNode addTwoNumbers(ListNode l1, ListNode l2) {
ListNode dummy=new ListNode();
ListNode cur=dummy;
int sum=0,carry=0;
while(l1!=null||l2!=null)
{
int num1=l1==null?0:l1.val;
int num2=l2==null?0:l2.val;
sum=num1+num2+carry;
carry=sum/10;
sum%=10;
cur.next=new ListNode(sum);
cur=cur.next;
if(l1!=null)
l1=l1.next;
if(l2!=null)
l2=l2.next;
}
if(carry!=0)//最后的进位不要忘记
cur.next=new ListNode(1);
return dummy.next;
}
}
//O(max(m,n)) m n分别为两个链表的长度
//O(1)
21. 合并两个有序链表
(https://leetcode-cn.com/problems/merge-two-sorted-lists/)
思路:向合并两个有序数组一样,同时从头遍历两条链表,比较当前的两个节点的值,取较小的值,并将较小的值所在链表的对应指针往后移动一次;当有某条链表提前遍历完时,只需要将剩余的节点直接连到答案节点上即可,无需再次遍历
class Solution {
public ListNode mergeTwoLists(ListNode list1, ListNode list2) {
ListNode dummy=new ListNode();
ListNode cur=dummy;
while(list1!=null&&list2!=null)
{
if(list1.val<=list2.val)
{
cur.next=list1;
list1=list1.next;
}
else
{
cur.next=list2;
list2=list2.next;
}
cur=cur.next;
}
if(list1!=null)
cur.next=list1;
if(list2!=null)
cur.next=list2;
return dummy.next;
}
}
//O(n)
//O(1)
19. 删除链表的倒数第 N 个结点
(https://leetcode-cn.com/problems/remove-nth-node-from-end-of-list/)
思路:使用快慢指针,先让快指针走n步,然后快慢指针一起走,这样当快指针走到最后一个节点时,慢指针刚好走到待删除节点的前一个节点; 注意:为了避免删除的节点是头结点这种情况,建议使用dummy节点,避免发生空指针异常
class Solution {
public ListNode removeNthFromEnd(ListNode head, int n) {
ListNode dummy=new ListNode();
dummy.next=head;
ListNode slow=dummy,fast=dummy;
for(int i=1;i<=n;i++)
fast=fast.next;
while(fast.next!=null)//快指针是否到达最后一个节点?
{
slow=slow.next;
fast=fast.next;
}
slow.next=slow.next.next;//删除节点
return dummy.next;//返回 注意返回的不是head 因为待删除的节点就是head节点
}
}
//O(n)
//O(1)
141. 环形链表
(https://leetcode-cn.com/problems/linked-list-cycle/)
思路:假设链表中有环,可以将这个环想象成1个环形跑道,如果有两个人在跑道上跑步,一个跑的快,一个跑的慢,假设跑的快的速度是跑的慢的速度的两倍,那么快的人一定可以在一圈内追上慢的人
简单来说:如果有环的话,快慢指针可以相遇
public class Solution {
public boolean hasCycle(ListNode head) {
ListNode slow=head,fast=head;
while(fast!=null&&fast.next!=null)
{
fast=fast.next.next;
slow=slow.next;
if(fast==null||slow==null)//到达null 说明没有环
return false;
if(fast==slow)//fast追上slow说明有环
return true;
}
return false;//无环
}
}
//O(n)
//O(1)
142. 环形链表 II
(https://leetcode-cn.com/problems/linked-list-cycle-ii/)
思路:先根据141题的思路判断链表是否存在环,即使用快慢指针,快指针的速度是慢指针的2倍,当相遇时,假设慢指针走了k步,则快指针走了2k步,由于二者最后位置相同,所以快指针多走的k步实际上就环的长度的整数倍(类似于操场跑步,可能多跑了1圈,也可能多跑了2圈);假设入环点到相遇点的距离是m,则起点到入环点的距离是k-m, 而从相遇点再走到入环点的距离也是k-m, 因此相遇后,让一个节点从起点开始走,一个节点从相遇点开始走,此时速度一样,则再次相遇时,相遇点就是入环点
public class Solution {
public ListNode detectCycle(ListNode head) {
if(head==null)
return null;
ListNode fast=head,slow=head;
boolean hasCircle=false;
while(fast!=null&&fast.next!=null){
fast=fast.next.next;
slow=slow.next;
if(fast==null||slow==null){//等于null说明没有环
return null;
}
if(fast==slow){//存在环 退出循环开始后面找环的入口
hasCircle=true;
break;
}
}
if(!hasCircle)
return null;
fast=head;//一个指向头节点
while(fast!=slow){//再次相遇的地方就是入环口
fast=fast.next;
slow=slow.next;
}
return fast;
}
}
//O(n)
//O(1)
206. 反转链表
(https://leetcode-cn.com/problems/reverse-linked-list/)
思路1:迭代
class Solution {
public ListNode reverseList(ListNode head) {
ListNode pre=null,cur=head;
while(cur!=null)
{
//顶针式写法
ListNode tmp=cur.next;
cur.next=pre;
pre=cur;
cur=tmp;
}
return pre;//最后返回pre
}
}
//O(n)
//O(1)
思路2:递归
class Solution {
public ListNode reverseList(ListNode head) {
if(head==null||head.next==null)
return head;
ListNode newHead=reverseList(head.next);//反转后n-1个节点得到的头结点
head.next.next=head;//比如1->2->3->4->5 后面4个节点已经反转得到1->2<-3<-4<-5
//现在需要2步操作 2.next=1 1.next=null 其中2=1.next 1是当前的旧head
head.next=null;
return newHead;
}
}
//O(n)
//O(n)
23. 合并K个升序链表
(https://leetcode-cn.com/problems/merge-k-sorted-lists/)
思路1:优先级队列,先将每个链表的头节点加入优先级队列中,然后取出队首的节点就是最小的,假设取出的节点是node,然后判断node后面还有没有节点,如果有则将node的下一个节点也加入队列,开始下一轮的选取,直到最后队列为空
class Solution {
public ListNode mergeKLists(ListNode[] lists) {
PriorityQueue<ListNode> pq=new PriorityQueue<>((a,b)->a.val-b.val);
for(ListNode head:lists){
if(head!=null){
pq.offer(head);
}
}
ListNode dummy=new ListNode(),p=dummy;
while(!pq.isEmpty()){
ListNode node=pq.poll();
p.next=node;
p=p.next;
if(node.next!=null){
pq.offer(node.next);
}
}
return dummy.next;
}
}
//(kn*logk) 最多有kn个节点 每个节点入队出队的时间logk
//O(k) 队列中的元素最多不会超过k个
思路2:根据合并两条有序链表的基础,将k条链表分治处理
如上图所示,划分到最小的子问题后,6条链表两两合并变成3条,然后在合并变成2条,最后变成1条,即最终的答案;假设每条链表的长度为n
第一轮合并: k 2 \frac{k}{2} 2k组 每组的节点数: n × 2 n\times 2 n×2 ---->nk
第二轮合并: k 4 \frac{k}{4} 4k组 每组的节点数: n × 4 n\times 4 n×4—>nk
第三轮合并: k 8 \frac{k}{8} 8k组 每组的节点数: n × 8 n \times 8 n×8—>nk
…
即每一层都需要nk次操作,一共有 l o g k log k logk层,故时间复杂度为: n k × l o g k nk\times logk nk×logk
空间复杂度为递归消耗的空间: l o g k log k logk
class Solution {
public ListNode mergeKLists(ListNode[] lists) {
return merge(lists,0,lists.length-1);
}
public ListNode merge(ListNode[] lists,int left,int right){
if(left==right){
return lists[left];
}
if(left>right){
return null;
}
int mid=(left+right)>>1;
return mergeTwoLists(merge(lists,left,mid),merge(lists,mid+1,right));
}
//合并两条有序的链表的模板方法
public ListNode mergeTwoLists(ListNode list1, ListNode list2) {
ListNode dummy=new ListNode();
ListNode cur=dummy;
while(list1!=null&&list2!=null)
{
if(list1.val<=list2.val)
{
cur.next=list1;
list1=list1.next;
}
else
{
cur.next=list2;
list2=list2.next;
}
cur=cur.next;
}
if(list1!=null)
cur.next=list1;
if(list2!=null)
cur.next=list2;
return dummy.next;
}
}
25. K 个一组翻转链表
(https://leetcode-cn.com/problems/reverse-nodes-in-k-group/)
思路:需要定义两个函数
ListNode getKthNode(ListNode cur,int k)
: 求以cur开始往后的第k个节点
boolean hasKNodes(ListNode cur,int k)
: 判断以cur开始的链表是否含有k个节点
cur先指向头节点,判断以cur开始的链表是否含有k个节点,没有直接返回,有的话进行反转
对于某一组(有K个节点),该组的第K个节点反转后会成为该组的头节点,该组的原来的头节点反转后会成为该组的最后一个节点,注意:反转后的最后一个节点应该指向下一组的第一个节点(下一组不足K个 未反转)或者指向下一组的最后一个节点(下一组足k个 可以反转),因此反转某组后,使该组反转后的最后一个节点指向下一组的第一个或最后一个节点
public ListNode reverseKGroup(ListNode head, int k) {
if(k==0||k==1)//0个1组或1个一组相当于不用反转
return head;
ListNode cur=head;
ListNode newHead=null;
if(hasKNodes(cur,k))//链表长度>=k
newHead=getKthNode(cur,k);
else //链表长度<k 相当于不用反转
return head;
ListNode pre=null,tmp;
while(cur!=null&&hasKNodes(cur, k)){
int i=0;
ListNode last=cur;//last保存当前组的第一个节点 反转之后last就是最后1个节点
while(i<k){//逆置某一组 和单链表的反转代码类似
tmp=cur.next;
cur.next=pre;
pre=cur;
cur=tmp;
i++;
}
last.next=getKthNode(cur,k);//逆置完后该组原来的第一个节点变成最后一个
//最后一个节点连接下一组的第k个节点(下一组有k个则和第k个连接 不足就跟第一个节点连接)
}
return newHead;
}
//获取从cur开始往后数的第k个节点
public ListNode getKthNode(ListNode cur,int k){
if(cur==null)
return null;
int i=1;
ListNode p=cur,pre=p;
while(p!=null&&i<k){
pre=p;
p=p.next;
i++;
}
return p==null?cur:p;//有k个则返回改组第k个节点 不足k个则返回改组第一个节点
}
//以cur节点开始是否还有k个节点?
public boolean hasKNodes(ListNode cur,int k){
int i=0;
while(cur!=null){
i++;
if(i==k)
break;
cur=cur.next;
}
return i==k;
}
//O(n)
//O(1)
138. 复制带随机指针的链表
https://leetcode-cn.com/problems/copy-list-with-random-pointer/
先根据已有节点的值创建一个具有相同值的节点,建立原有节点和新节点的映射,先不考虑next和random指针创建完映射之后,对于原始节点X,对应的新创建的节点Y=map.get(X) Y.next就是map.get(X.next) Y.random就是map.get(X.random) X.next和X.random都是指向原有的节点,原有的节点是map中的键,因此都对应一个新创建的节点
class Solution {
HashMap<Node,Node> map;
public Node copyRandomList(Node head) {
map=new HashMap<>();
Node cur=head;
while(cur!=null){
map.put(cur,new Node(cur.val));
cur=cur.next;
}
cur=head;
while(cur!=null){
Node cloneNode=map.get(cur);
cloneNode.next=map.get(cur.next);
cloneNode.random=map.get(cur.random);
cur=cur.next;
}
return map.get(head);
}
}
//O(n)
//O(n)
24. 两两交换链表中的节点
https://leetcode-cn.com/problems/swap-nodes-in-pairs/
//迭代
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode() {}
* ListNode(int val) { this.val = val; }
* ListNode(int val, ListNode next) { this.val = val; this.next = next; }
* }
*/
class Solution {
public ListNode swapPairs(ListNode head) {
if(head==null||head.next==null)//链表为空或者只有1个节点
return head;
ListNode dummy=new ListNode();
dummy.next=head;
ListNode tmp=dummy;//创建哑节点 方便操作
while(tmp.next!=null&&tmp.next.next!=null){//有2个节点
ListNode node1=tmp.next;
ListNode node2=tmp.next.next;
tmp.next=node2;//第2个节点作为头节点
node1.next=node2.next;//第一个节点指向第3个节点
node2.next=node1;
tmp=node1;
}
return dummy.next;
}
}
//O(n)
//O(1)
//递归
class Solution {
public ListNode swapPairs(ListNode head) {
if(head==null||head.next==null)//链表为空或者只有1个节点
return head;
ListNode newHead=head.next;//第2个节点是新的头节点
head.next=swapPairs(newHead.next);//第1个节点指向第3个节点以后反转后的头节点
newHead.next=head;//第2个节点指向第1个节点
return newHead;
}
}
//O(n)
//O(n)
剑指 Offer II 029. 排序的循环链表
https://leetcode.cn/problems/4ueAj6/
思路:遍历两次链表,第一次遍历找出链表中的最小值节点和最大值节点,第2次遍历找出新加入的节点的待插入位置,然后将新节点插入即可,细节处理见代码注释
/*
// Definition for a Node.
class Node {
public int val;
public Node next;
public Node() {}
public Node(int _val) {
val = _val;
}
public Node(int _val, Node _next) {
val = _val;
next = _next;
}
};
*/
class Solution {
public Node insert(Node head, int insertVal) {
if(head==null){
Node newNode=new Node(insertVal);
newNode.next=newNode;
return newNode;
}
Node pre=head,cur=head.next;
while(pre.val<=cur.val&&cur!=head){//cur!=head 防止1 1 1这种情况出现死循环
pre=cur;
cur=cur.next;
}
Node first=cur;
boolean flag=false;
//cur指向最小的节点(第一个) pre指向最大的节点(最后一个) 下面的while如果不执行说明
//insertVal是当前最小的值 直接连在pre后面
while(cur.val<insertVal){
pre=cur;
cur=cur.next;
//下面的判断防止当insertVal是当前一个比较大的值导致死循环 链表中找不到比其小的值
//此时遍历一次链表就停止
if(flag&&cur==first){//第2次cur=first说明遍历一次结束
break;
}
if(!flag){//设置第2次访问标记
flag=true;
}
}
//cur是newNode的下一个节点 pre是newNode的上一个节点
Node newNode=new Node(insertVal);
newNode.next=cur;
pre.next=newNode;
return head;
}
}
//O(n)
//O(1)