OptimalSolution(3)--链表问题(1)简单
单链表Node节点类
public class Node { public int val; public Node next; public Node(int val) { this.val = val; } }
双链表DoubleNode类
public class DoubleNode { public int val; public DoubleNode last; public DoubleNode next; public DoubleNode(int val) { this.val = val; } }
一、打印两个有序链表的公共部分
问题:给定两个有序链表head1和head2,打印两个链表的公共部分
解答:(1)如果head1的值小于head2,则head1向下移动。(2)如果head2的值小于head1的值,则head2向下移动。(3)如果head1的值等于head2的值,则打印这个值,然后head1和head2都往下移动。(4)head1或head2有任意一个移动到null,则整个过程停止
public void printCommonPart(Node head1, Node head2) { while (head1 != null && head2 != null) { if (head1.val < head2.val) { head1 = head1.next; } else if (head1.val > head2.val) { head2 = head2.next; } else { System.out.print(head1.val + " "); head1 = head1.next; head2 = head2.next; } } System.out.println(); }
二、在单链表和双链表中删除倒数第K个节点
题目1:删除单链表中倒数第K个节点
在删除问题中,如果想要删除某一个节点,就必须定位到要删除节点的前一个节点。
解答:
(1)如果链表为空或者K值小于1,则直接返回即可。否则让链表从头开始走到尾,每移动一步,就让K值减1,链表走到结尾时,判断K值的大小
(2)如果K值大于0(1→2→3,K=4,K:3 2 1),说明链表没有倒数第K个节点,直接返回即可。
(3)如果K值等于0(1→2→3,K=3,K:2 1 0),说明倒数第K个节点就是头节点,直接删除头节点,返回head.next即可
(4)如果K值小于0(1→2→3,K=2,K:1 0 -1),此时,重新从头节点开始走,每移动一步,就让K的值加1,当K等于0时,移动停止,此时的节点就是要删除节点的前一个节点。(假设链表长度为N,那么倒数第K个节点的前一个节点就是第N-K个节点,第一次遍历后,K变为K-N,第二次遍历后,遍历到第N-K个节点时K为0)
public Node removeLastKthNode(Node head, int lastKth) { if (head == null || lastKth < 1) { return head; } Node cur = head; while (cur != null) { lastKth--; cur = cur.next; } if (lastKth == 0) { head = head.next; } if (lastKth < 0) { cur = head; while (++lastKth != 0) { cur = cur.next; } cur.next = cur.next.next; } return head; }
题目2:删除双链表中倒数第K个节点(和单链表同理,只是注意last指针的重连即可)
public DoubleNode removeLastKthNode(DoubleNode head, int lastKth) { if (head == null || lastKth < 1) { return head; } DoubleNode cur = head; while (cur != null) { lastKth--; cur = cur.next; } if (lastKth == 0) { head = head.next; head.last = null; } if (lastKth < 0) { cur = head; while (++lastKth != 0) { cur = cur.next; } DoubleNode newNext = cur.next.next; cur.next = newNext; if (newNext != null) { newNext.last = cur; } } return head; }
三、删除链表的中间节点和a/b处的节点
题目1:删除链表的中间节点,例如1→2删除节点1,1→2→3删除节点2,1→2→3→4删除节点2,1→2→3→4→5删除节点3
解答:如果链表为空或长度为1,则直接返回,如果链表长度为2,删除头节点即可,则3-2,4-2,5-3,6-3...,也就是长度每增加2,要删除的节点就后移一个节点。
public Node removeMidNode(Node head) { if (head == null || head.next == null) { return head; } if (head.next.next == null) { return head.next; } Node pre = head; Node cur = head.next.next; while (cur.next != null && cur.next.next != null) { pre = pre.next; cur = cur.next.next; } pre.next = pre.next.next; return head; }
题目2:删除位于a/b处节点,链表1→2→3→4→5,假设a/b=r,则r==0不删除任何节点,r在(0,1/5]上删除节点1,r在(1/5,2/5]删除节点2,如果r大于1,不删除任何节点
解答:根据链表长度n,以及a和b先计算出n*(a/b)的值,然后r向上取整就是要删除的节点的位置。
public Node removeByRatio(Node head, int a, int b) { if (a < 1 || a > b) { return head; } int n = 0; Node cur = head; while (cur != null) { n++; cur = cur.next; } n = (int)Math.ceil((double) (a * n) / (double)b); if (n == 1) { head = head.next; } if (n > 1) { cur = head; while (--n != 1) { cur = cur.next; } cur.next = cur.next.next; } return head; }
四、反转单向和双向链表
题目1:反转单向链表
public Node reverseList(Node head) { Node pre = null; Node next = null; while (head != null) { next = head.next; head.next = pre; pre = head; head = next; } return pre; }
题目2:反转双向链表
public DoubleNode reverseList(DoubleNode head) { DoubleNode pre = null; DoubleNode next = null; while (head != null) { next = head.next; head.next = pre; head.last = next; pre = head; head = next; } return pre; }
五、反转部分单向链表
题目:给定一个单链表和两个整数from和to,把第from个到第to个节点这一部分进行反转。例如1→2→3→4→5→null,from=2,to=4,则1→4→3→2→5→null
解答:
(1)如果1<=from<=to<=N不满足,返回原来的头节点
(2)找到第from-1个节点fPre和第to+1个节点tPos,把反转的部分先反转,然后正确连接fPre和tPos
(3)如果fPre为null,说明反转部分是包含头节点的,返回每反转以前反转部分的最后一个节点作为新的头节点,如果fPre不为null,返回旧的头节点。
public Node reversePart(Node head, int from, int to) { int len = 0; Node node1 = head; Node fPre = null; Node tPos = null; while (node1 != null) { len++; fPre = len == from - 1 ? node1 : fPre; tPos = len == to + 1 ? node1 : tPos; node1 = node1.next; } if (from > to || from < 1 || to > len) { return head; } node1 = fPre == null ? head : fPre.next; Node node2 = node1.next; node1.next = tPos; Node next = null; while (node2 != tPos) { next = node2.next; node2.next = node1; node1 = node2; node2 = next; } if (fPre != null) { fPre.next = node1; return head; } return node1; }
六、两个单链表生成相加链表
问题:9→3→7代表整数937,6→3代表整数63,将两个链表相加得到937+63=1000,返回1→0→0→0
思路:如果先分别算出各自链表所代表的整数,那么链表的长度很长,可以表示一个很大的数,转换成int时可能会异常
解法1:利用栈结构
(1)将两个链表从左到右遍历,并压入各自的栈中。因此s1:9 3 7,s2:6 3,
(2)将s1和s2同步弹出,然后相加链表即可,同时还需要关注是否有进位
(3)当s1和s2都为空时,如果进位信息为1,还要生成一个节点值为1的新结点
注意:两个整数相加与进位问题。1. n1 = s1.isEmpty() ? 0 : s1.pop();2.n = n1 + n2 + ca;3.node = new Node(n % 10);4.ca = n / 10;
注意:从后往前生成链表的过程。 pre = node; node = new Node(n % 10); node.next = pre;
public Node addLists1(Node head1, Node head2) { Stack<Integer> s1 = new Stack<>(); Stack<Integer> s2 = new Stack<>(); while (head1 != null) { s1.push(head1.val); head1 = head1.next; } while (head2 != null) { s2.push(head2.val); head2 = head2.next; } int ca = 0; int n1 = 0, n2 = 0; int n = 0; Node node = null; Node pre = null; while (!s1.isEmpty() || !s2.isEmpty()) { n1 = s1.isEmpty() ? 0 : s1.pop(); n2 = s2.isEmpty() ? 0 : s2.pop(); n = n1 + n2 + ca; pre = node; node = new Node(n % 10); node.next = pre; ca = n / 10; } if (ca == 1) { pre = node; node = new Node(1); node.next = pre; } return node; }
解法2:利用链表的逆序
(1)将两个链表逆序,依次得到从低位到高位的数字
(2)遍历两个逆序后的链表,过程和解法1类似
需要注意:1.1 = c1 != null ? c1.val : 0; 2.c1 = c1 != null ? c1.next : null; 3.完成后还需要reverseList(head1); reverseList(head2);将原来的链表返回原状。
public Node addList2(Node head1, Node head2) { head1 = reverseList(head1); head2 = reverseList(head2); int ca = 0, n1 = 0, n2 = 0, n = 0; Node c1 = head1; Node c2 = head2; Node node = null; Node pre = null; while (c1 != null || c2 != null) { n1 = c1 != null ? c1.val : 0; n2 = c2 != null ? c2.val : 0; n = n1 + n2 + ca; pre = node; node = new Node(n%10); node.next = pre; ca = n / 10; c1 = c1 != null ? c1.next : null; c2 = c2 != null ? c2.next : null; } if (ca == 1) { pre = node; node = new Node(1); node.next = pre; } reverseList(head1); reverseList(head2); return node; }
七、删除无序单链表中值重复出现的节点
问题:给定一个无序单链表,删除其中值重复出现的节点,例如1→2→3→3→4→4→2→1→1→null,返回1→2→3→4→null。
解法1:利用哈希表,但是时间复杂度为O(N),空间复杂度为O(N)
最近一个没有被删除的节点为pre,当前节点cur的值如果出现过,就令pre.next=cur.next;如果没出现过,就将cur的值加入到哈希表,同时令pre=cur。
public void removeRep1(Node head) { if (head == null) { return; } HashSet<Integer> set = new HashSet<>(); Node pre = head; Node cur = head.next; set.add(head.val); while (cur != null) { if (set.contains(cur.val)) { pre.next = cur.next; } else { set.add(cur.val); pre = cur; } cur = cur.next; } }
解法2:利用选择排序的过程,时间复杂度为O(N2),空间复杂度为O(1)(直接选择排序时间复杂度为O(N2))
例如1→2→3→3→4→4→2→1→1→null,(1)cur值为1,检查后面所有值为1的节点全部删除(2)cur值为2,检查后面值为2的节点全部删除...依次类推
public void removeRep2(Node head) { Node cur = head; Node pre = null; Node next = null; while(cur != null) { pre = cur; next = cur.next; while (next != null) { if (cur.val == next.val) { pre.next = next.next; } else { pre = next; } next = next.next; } cur = cur.next; }
八、在单链表删除指定值的节点
题目:给定一个单链表和一个整数num,删除值为num的全部节点。例如:1→2→3→4→null,num=3,返回:1→2→4→null
方法1:O(N)+O(N)利用栈或者其他容器收集值不等于num的节点,收集完成后重新连接即可。最后返回栈底节点为新的头结点
public Node removeValue1(Node head, int num) { Stack<Node> stack = new Stack<>(); while(head != null) { if (head.val != num) { stack.push(head); } head = head.next; } while (!stack.isEmpty()) { stack.peek().next = head; // head一开始为null head = stack.pop(); } return head; }
方法2:O(N)+O(1)不用任何容器直接调整。找到第一个不为num的节点作为新的头结点newHead,继续往后遍历,如果cur的值等于num,就将cur删除,即令最近一个值不等于num的pre.next=cur.next,如果不等于,就令pre=cur
public Node removeValue2(Node head, int num) { while (head != null) { if (head.val != num) { break; } head = head.next; } Node pre = head; Node cur = head; while(cur != null) { if (cur.val == num) { pre.next = cur.next; } else { pre = cur; } cur = cur.next; } return head; }
九、单链表的选择排序
题目:给定一个无序单链表,实现单链表的选择排序,要求空间复杂度为O(1)
选择排序就是:从未排序的部分中找到最小值,然后放在排好序部分的尾部,逐渐将未排序的部分缩小,最后全部变成排好序的部分。
解法:O(N2)+O(1)
(1)找到最小值节点,将其设置成新的头节点newHead
(2)每次在未排序的部分中找到最小值的节点,然后把这个节点从未排序的链表中删除,同时连接删除节点的前后节点
(3)把删除的节点连接在排好序部分的链表尾部
public Node getSmallestPreNode(Node head) { Node smallPre = null; Node small = head; Node pre = head; Node cur = head.next; while (cur != null) { if (cur.val < small.val) { smallPre = pre; small = cur; } pre = cur; cur = cur.next; } return smallPre;// 返回以head为头结点的单链表中最小值节点的前一个节点 } public Node selectionSort(Node head) { Node tail = null; // 已排序部分尾部 Node cur = head; // 未排序部分头部 Node smallPre = null; // 最小节点的前一个节点 Node small = null; // 值最小的节点 while (cur != null) { small = cur; smallPre = getSmallestPreNode(cur); if (smallPre != null) { // small为未排序部分的最小值 small = smallPre.next; // 将最小值节点从未排序的部分删除 smallPre.next = small.next; } // smallPre为null时,说明当前节点cur已经是最小值,因此在原链表中删除头节点 cur = cur == small ? cur.next : cur; if (tail == null) { head = small; } else { tail.next = small; } tail = small; } return head; }
十、一种怪异的节点删除方式
问题:给定一个链表的节点node,但不给定整个整个链表的头节点,如何在链表中删除node?并分析出现的问题
思路:例如1→2→3→null,如果要删除节点2,那么在仅给定节点2的情况下,将节点2的值变成3,然后删除结点3即可。(在链表中删除节点的原则,必须定位到待删节点的上一个节点)
可能出现的问题:
(1)无法删除最后一个节点。因为如果要删除节点3,由于根本没有下一个节点来代替节点3被删除,那么只有让节点2的next指向null这一种办法,但是又根本找不到节点2,所以就无法正确删除节点3。
(2)这种删除方式在本质上根本不是删除了node节点,而是把node节点的值改变,然后删除node的下一个节点。在实际工程中,一个节点可能代表很复杂的结构,节点的复制会相当复杂,或者可能改变节点值这个操作都是被禁止的;或者工程上一节点代表提供服务的一个服务器,外界对每个节点都有很多依赖,例如,在删除节点2时,实际上影响了节点3对外提供的服务。
注意:异常的抛出,RuntimeException异常类不需要导包。
public void removeNodeWired(Node node) { if (node == null) { return; } Node next = node.next; if (next == null) { throw new RuntimeException("can not remove last node."); } node.val = next.val; node.next = next.next; }
十一、向有序的环形单链表中插入新节点
问题:一个环形单链表从头节点head开始不降序,同时由最后的节点指向头节点。给定一个环形单链表的头节点和一个整数num,生成节点值为num的新节点,并插入到这个环形链表中,保证调整后的链表依然有序。
解法:
(1)生成节点值为num的新节点,记为node
(2)如果链表为空,直接返回node
(3)令pre=head,cur=head.next,令pre和cur同步移动,如果pre的节点值小于或等于num,并且cur的节点值大于或等于num,说明node应该在pre和cur节点之间插入
(4)如果pre和cur转了一圈都没有发现插入位置,说明node应该插入到头节点的前面,此时是因为链表中每个节点都大于num或者都小于num。如果num比每个节点值都大,返回原来的头节点即可;如果num比每个节点的值都小,应该把node作为链表新的头节点返回。
public Node insertNum(Node head, int num) { Node node = new Node(num); if (head == null) { node.next = node; return node; } Node pre = head; Node cur = head.next; while (cur != head) { if (pre.val <= num && cur.val >= num) { break; } pre = cur; cur = cur.next; } pre.next = node; node.next = cur; return head.val < num ? head : node; }
十二、合并两个有序的单链表
题目:给定两个有序单链表head1和head2,返回合并后的链表。例如:0→2→3→7→null,1→3→5→7→9→null,返回0→1→2→3→3→5→7→7→9→null
解法:(O(M+N) + O(1))
(1)如果两个链表有一个为空,返回非空链表头节点
(2)head1和head2中小的节点就是新链表的头节点
(3)每次比较遍历到的值,根据大小关系作出调整,同时用一个变量pre表示上次比较时值较小的节点
链表1:1→5→6→null, 链表2:2→3→7→null
cur1=1,cur2=2,pre=null,cur1小于cur2,不作调整,pre=cur1
cur1=5,cur2=2,pre=1,cur2小于cur1,pre.next指向cur2,cur2.next指向cur1,pre=2,此时1→2→5→6→null,3→7→null,
cur1=5,cur2=3,pre=2,cur2小于cur1,pre.next指向cur2,cur2.next指向cur1,pre=3,此时1→2→3→5→6→null,7→null
cur1=5,cur2=7,pre=3,cur1小于cur2,不作调整,pre=5
cur1=6,cur2=7,pre=5,cur1小于cur2,不作调整,pre=6,
此时cur1=null,pre为链表1的最后一个节点,把pre.next指向cur2
(4)如果链表1先走完,此时cur1=null,pre为链表1的最后一个节点,那么就把pre的next指针指向链表2的当前节点,即cur2;如果链表2先走完,说明链表2的所有节点已经插入到链表1中,此时返回新的头节点即可。
注意:pre这个变量在这道题里面所起到的作用。 pre.next = cur1 == null ? cur2 : cur1;
public Node merge(Node head1, Node head2) { if (head1 == null || head2 == null) { return head1 != null ? head1 : head2; } Node head = head1.val < head2.val ? head1 : head2; Node cur1 = head == head1 ? head1 : head2; Node cur2 = head == head1 ? head2 : head1; Node pre = null; Node next = null; while (cur1 != null && cur2 != null) { if (cur1.val <= cur2.val) { pre = cur1; cur1 = cur1.next; } else { next = cur2.next; pre.next = cur2; cur2.next = cur1; pre = cur2; cur2 = next; } } pre.next = cur1 == null ? cur2 : cur1; return head; }
十三、按照左右半区的方式重新组合单链表
问题:给定一个长度为N的单链表,如果N为偶数,那么前N/2个节点为左半区,后N/2个节点为右半区;如果N为奇数,那么前N/2个节点算作左半区,后N/2+1个节点为右半区,将单链表调整成L1→R1→L2→R2..的形式。例如:1→2→3→4→null调整为1→3→2→4→null,1→2→3→4→5→null调整为1→3→2→4→5→null
解法:O(N)+O(1)
(1)如果链表为空或长度为1,则直接返回
(2)遍历一遍找到左半区的最后一个节点,记为mid,这里涉及到之前做过的题,求中间节点的方法:即从长度为2开始,长度每增加2,mid就向后移动一个节点。例如:12mid为1,123mid为1,1234mid为2,12345mid为2,123456mid为3...
(3)将左右半区分离成2个链表即mid.next=null,头结点分别为原来的head记为left,和mid.next记为right。
注意:1.mergeLR方法是将right链表的每一个节点依次插入到left的合适位置中。
2.求链表中间节点mid的方法: Node mid = head; Node right = head.next; while (right.next != null && right.next.next != null) { mid = mid.next; right = right.next.next; }
public void relocate(Node head) { if (head == null || head.next == null) { return; } Node mid = head; Node right = head.next; while (right.next != null && right.next.next != null) { mid = mid.next; right = right.next.next; } right = mid.next; mid.next = null; mergeLR(head, right); } public void mergeLR(Node left, Node right) { Node next = null; while (left.next != null) { next = right.next; right.next = left.next; left.next = right; left = right.next; right = next; } left.next = right; }