1.编写代码,移除未排序链表中的重复结点。如果不得使用临时缓冲区,该怎么解决?
思路:直接迭代访问整个链表,将每个结点加入散列表。若发现有重复元素,则将该结点从链表中移除,然后继续迭代。使用链表。只需扫描一次即可。时间复杂度是o(N),N是链表结点数目。
import java.util.Hashtable; class LinkedListNode { int data; LinkedListNode next; } public class DeleteNodes { public static void main(String[] args) { // TODO Auto-generated method stub LinkedListNode head = new LinkedListNode(); head.data = 1; LinkedListNode node = new LinkedListNode(); node.data = 1; head.next = node; LinkedListNode tmp = head; while (tmp != null) { System.out.println(tmp.data); tmp = tmp.next; } tmp = deleteDups(head); System.out.println("after delete"); while (tmp != null) { System.out.println(tmp.data); tmp = tmp.next; } } //使用临时缓冲区 public static LinkedListNode deleteDups(LinkedListNode n) { Hashtable table = new Hashtable(); LinkedListNode head = n;//保存头结点 LinkedListNode current = head; while (n != null) { if (table.containsKey(n.data)) { current.next = n.next; } else { table.put(n.data, true); current = n; } n = n.next; } return head; } }
如不借助额外缓冲区,可以用两个指针来迭代:current迭代访问整个链表,runner用于检查后续的结点是否重复。时间复杂度为o(N的平方),空间复杂度为o(1)。
import java.util.Hashtable; class LinkedListNode { int data; LinkedListNode next; } public class DeleteNodes { public static void main(String[] args) { // TODO Auto-generated method stub LinkedListNode head = new LinkedListNode(); head.data = 1; LinkedListNode node = new LinkedListNode(); node.data = 1; head.next = node; LinkedListNode tmp = head; while (tmp != null) { System.out.println(tmp.data); tmp = tmp.next; } tmp = deleteDups(head); System.out.println("after delete"); while (tmp != null) { System.out.println(tmp.data); tmp = tmp.next; } } //不使用临时缓冲区 public static LinkedListNode deleteDups(LinkedListNode n) { if (n == null) { return n; } LinkedListNode head = n; LinkedListNode current = head; while (current != null) { LinkedListNode runner = current; while (runner.next != null) { if (runner.next.data == current.data) { runner.next = runner.next.next; } else { runner = runner.next; } } current = current.next; } return head; } }
1.1删除排序链表中的重复结点
在一个排序的链表中,存在重复的结点,请删除该链表中重复的结点,重复的结点出现一次,返回链表头指针。 例如,链表1->2->3->3->4->4->5 处理后为 1->2->3->4->5
思路:遍历一次,遇到重复的节点删除(将前一节点的指针指向该节点的下一个节点)即可。
public class Solution { public ListNode deleteDuplicates(ListNode head) { if (head == null) { return null; } ListNode node = head; while (node.next != null) { if (node.val == node.next.val) { node.next = node.next.next; } else { node = node.next; } } return head; } }
1.2删除排序链表中的重复结点II
在一个排序的链表中,存在重复的结点,请删除该链表中重复的结点,重复的结点不保留,返回链表头指针。 例如,链表1->2->3->3->4->4->5 处理后为 1->2->5
思路:
1.先给原链表添加一个头结点first方便处理;
2.使用3个指针,一个指向前一个节点pre,一个指向当前节点pHead,一个指向下一个节点pHead->next。
3.当当前节点跟后一个节点相等时,不断往后遍历,找到第一个不等于当前节点的节点,然后用 pre 指向它;
当当前节点跟后一个不相等时,将pre 后移指向pHead,pHead后移一位.。
public class Solution { public ListNode deleteDuplication(ListNode pHead) { ListNode first = new ListNode(Integer.MIN_VALUE); ListNode pre; first.next = pHead; pre = first; while(pHead != null && pHead.next != null) { if (pHead.val == pHead.next.val) { int val = pHead.val; while(pHead != null && pHead.val == val) { pHead = pHead.next; } pre.next = pHead; } else { pre = pHead; pHead = pHead.next; } } return first.next; } }
2.实现一个算法,找出单向链表中倒数第k个结点。
注意:k定义如下:传入k=1将返回最后一个结点,k=2返回倒数第二个结点,以此类推。
解法1:链表长度已知
若链表长度已知,倒数第k个结点就是第(length - k)个结点,直接迭代访问链表即可,但是过于简单!
解法2:递归
对问题略作调整,只打印倒数第k个结点的值。
public class NodeLast { public static void main(String[] args) { // TODO Auto-generated method stub LinkedListNode head = new LinkedListNode(); head.data = 1; LinkedListNode node = new LinkedListNode(); node.data = 2; head.next = node; LinkedListNode node1 = new LinkedListNode(); node1.data = 3; node.next = node1; nthToLast(head, 2); } //递归解法 public static int nthToLast(LinkedListNode head, int k) { if (head == null) { return 0; } int i = nthToLast(head.next, k) + 1; if (i == k) { System.out.println(head.data); } return i; } }
解法3:迭代:使用双指针p1和p2,并将它们指向链表中相距k个结点的两个结点。【要注意p2移动时i < k - 1】
先将p1和p2指向链表首结点,然后将p2向前移动k个结点。之后,我们以相同的速度移动这两个指针,p2会在移动length-k步后抵达链表尾结点。此时,p1会指向链表第length-k个结点,或者说倒数第k个结点。
public class NodeLast { public static void main(String[] args) { // TODO Auto-generated method stub LinkedListNode head = new LinkedListNode(); head.data = 1; LinkedListNode node = new LinkedListNode(); node.data = 2; head.next = node; LinkedListNode node1 = new LinkedListNode(); node1.data = 3; node.next = node1; LinkedListNode node2 = nthToLast(head, 2); System.out.println(node2.data); } //迭代解法 public static LinkedListNode nthToLast(LinkedListNode head, int k) { if (head == null || k <= 0) { return null; } LinkedListNode p1 = head; LinkedListNode p2 = head; // Move p2 k nodes into the list. Keep p1 in the same position. for (int i = 0; i < k - 1; i++) { if (p2 == null) { return null; // Error: list is too small. } p2 = p2.next; } if (p2 == null) { // Another error check. return null; } // Move them at the same pace. When p2 hits the end, // p1 will be at the right element. while (p2.next != null) { p1 = p1.next; p2 = p2.next; } return p1; } }
3.实现一个算法,删除单向链表中间的某个结点,假定你只能访问该结点。
解法:直接将后继结点的数据复制到当前结点,然后删除这个后续结点。
public class DeleteNode { public static void main(String[] args) { // TODO Auto-generated method stub } public static boolean deleteNode(LinkedListNode n) { if (n == null || n.next == null) {//n.next==null表示待删除的结点为链表的尾结点,这个问题无解。 return false; } LinkedListNode next = n.next; n.data = next.data; n.next = next.next; return true; } }
4.编写代码,以给定值x为基准将链表分割成两部分,所有小于x的结点排在大于或等于x的结点之前。
解法一:直接创建两个链表:一个链表存放小于x的元素;另一个链表存放大于或等于x的元素。迭代访问整个链表,将元素插入before或after链表。一旦抵达链表末端,则表明拆分完成,最后合并两个链表。注意保存当前操作结点的后继结点然后把当前结点的后续结点置为0这一操作。
public class Partition { public static void main(String[] args) { // TODO Auto-generated method stub LinkedListNode head = new LinkedListNode(); head.data = 3; LinkedListNode node = new LinkedListNode(); node.data = 2; head.next = node; LinkedListNode node1 = new LinkedListNode(); node1.data = 1; node.next = node1; LinkedListNode tmp = head; while(tmp != null){ System.out.println(tmp.data); tmp = tmp.next; } tmp = partition(head,2); System.out.println("after change"); while(tmp != null){ System.out.println(tmp.data); tmp = tmp.next; } } public static LinkedListNode partition(LinkedListNode node, int x) { LinkedListNode beforeStart = null; LinkedListNode beforeEnd = null; LinkedListNode afterStart = null; LinkedListNode afterEnd = null; while (node != null) { LinkedListNode next = node.next;//使用临时结点记下后续结点,以便下一次迭代时使用 node.next = null;//将当前结点的后继结点置为 0 if (node.data < x) { if (beforeStart == null) { beforeStart = node; beforeEnd = beforeStart; } else { beforeEnd.next = node; beforeEnd = node; } } else { if (afterStart == null) { afterStart = node; afterEnd = afterStart; } else { afterEnd.next = node; afterEnd = node; } } node = next; } if (beforeStart == null) { return afterStart; } beforeEnd.next = afterStart; return beforeStart; } }
解法二:与第一种解法略有不同,结点不再追加至before和after链表的末端,而是插入这两个链表的前端。
public class Partition { public static void main(String[] args) { // TODO Auto-generated method stub LinkedListNode head = new LinkedListNode(); head.data = 3; LinkedListNode node = new LinkedListNode(); node.data = 2; head.next = node; LinkedListNode node1 = new LinkedListNode(); node1.data = 1; node.next = node1; LinkedListNode tmp = head; while(tmp != null){ System.out.println(tmp.data); tmp = tmp.next; } tmp = partition(head,2); System.out.println("after change"); while(tmp != null){ System.out.println(tmp.data); tmp = tmp.next; } } //解法二 public static LinkedListNode partition(LinkedListNode node, int x) { LinkedListNode beforeStart = null; LinkedListNode afterStart = null; while (node != null) { LinkedListNode next = node.next;//使用临时结点记下后续结点,以便下一次迭代时使用 if (node.data < x) { //将结点插入before链表的前端 node.next = beforeStart; beforeStart = node; } else { node.next = afterStart; afterStart = node; } node = next; } if (beforeStart == null) { return afterStart; } //找到beforeStart链表的末尾,合并两个链表 LinkedListNode head = beforeStart; while (beforeStart.next != null) { beforeStart = beforeStart.next; } beforeStart.next = afterStart; return head; } }
5.给定两个用链表表示的整数,每个结点包含一个数位。这些数位是反向存放的,也就是个位排在链表首部。编写函数对这两个整数求和,并用链表形式返回结果。假设这些数位是正向存放的,请再做一遍。
解法:递归地模拟将两个结点的值逐一相加,如有进位则转入下一个结点这一过程。
class ListNode{ int data; ListNode next; ListNode(int x){ data = x; } } public class AddLists { public static void main(String[] args){ // TODO Auto-generated method stub ListNode head = new ListNode(1); ListNode node = new ListNode(2); head.next = node; ListNode node1 = new ListNode(3); node.next = node1; ListNode tmp = head; while(tmp != null){ System.out.println(tmp.data); tmp = tmp.next; } tmp = sum(head,head); while(tmp != null){ System.out.println(tmp.data); tmp = tmp.next; } } public static ListNode sum (ListNode head1, ListNode head2){ if(head1 == null) return head2; if(head2 == null) return head1; int n1 = 0; int n2 = 0; int sum = 0; int flag = 0; while (head1 != null) { n1 += head1.data * Math.pow(10, flag++); head1 = head1.next; } System.out.println("n1 = " + n1); flag = 0; while (head2 != null){ n2 += head2.data * Math.pow(10, flag++); head2 = head2.next; } System.out.println("n2 = " + n2); sum = n1 + n2; System.out.println("sum = " + sum); int digit = sum % 10; ListNode head = new ListNode(digit); ListNode res = head; sum /= 10; while (sum != 0) { digit = sum % 10; ListNode n = new ListNode(digit); head.next = n; head = head.next; sum /= 10; } return res; } }
进阶解法:
(1)一个链表的结点可能比另一个链表少,可以一开始先比较两个链表的长度并用零填充较短的链表。
(2)在前一个问题中,相加的结果不断追加到链表尾部(也即向前传递)。这就意味着递归调用会传入进位,而且会返回结果(随后追加到链表尾部)。
进阶问题中的结果要加到首部(也即向后传递),递归调用必须返回结果和进位,通过创建一个PartialSum包裹类来解决这一点。
class ListNode1{ int data; ListNode1 prev; ListNode1 next; ListNode1(int x, ListNode1 prev, ListNode1 next){ data = x; this.prev = prev; this.next = next; } } class PartialSum { public ListNode1 sum = null; public int carry = 0; } public class AddListsMore { /*public static class PartialSum { public ListNode1 sum = null; public int carry = 0; }*/ public static void main(String[] args) { // TODO Auto-generated method stub ListNode1 head = new ListNode1(1, null, null); ListNode1 node = new ListNode1(2, null, null); head.next = node; ListNode1 node1 = new ListNode1(3, null, null); node.next = node1; ListNode1 head1 = new ListNode1(4, null, null); ListNode1 node2 = new ListNode1(5, null, null); head1.next = node2; ListNode1 node3 = new ListNode1(6, null, null); node2.next = node3; ListNode1 node4 = new ListNode1(7, null, null); node3.next = node4; ListNode1 tmp = new ListNode1(0, null, null); tmp = addLists(head,head1); while (tmp != null) { System.out.println(tmp.data); tmp = tmp.next; } } public static ListNode1 addLists(ListNode1 l1, ListNode1 l2) { int len1 = length(l1); int len2 = length(l2); //用零填充较短的链表 if (len1 < len2) { l1 = padList(l1, len2 - len1); } else { l2 = padList(l2, len1 - len2); } //对两个链表求和 PartialSum sum = addListsHelper(l1, l2); //如有进位,则插入链表首部,否则,直接返回整个链表 if (sum.carry == 0) { return sum.sum; } else { ListNode1 result = insertBefore(sum.sum, sum.carry); return result; } } //用零填充链表 private static ListNode1 padList(ListNode1 l, int padding) { ListNode1 head = l; for (int i = 0; i < padding; i++) { ListNode1 n = new ListNode1(0, null, null); head.prev = n; n.next = head; head = n; } return head; } //辅助函数,将结点插入链表首部 private static ListNode1 insertBefore(ListNode1 list, int data) { ListNode1 node = new ListNode1(data, null, null); if (list != null) { list.prev = node; node.next = list; } return node; } private static PartialSum addListsHelper(ListNode1 l1, ListNode1 l2) { if (l1 == null && l2 == null) { PartialSum sum = new PartialSum(); return sum; } //对较小数字递归求和 PartialSum sum = addListsHelper(l1.next, l2.next); //将进位和当前数据相加 int val = sum.carry + l1.data + l2.data; //插入当前数字的求和结果 ListNode1 full_result = insertBefore(sum.sum, val % 10); //返回求和结果和进位值 sum.sum = full_result; sum.carry = val / 10; return sum; } //返回链表长度 private static int length(ListNode1 l) { if (l == null) { return 0; } else { return 1 + length(l.next); } } }
6.给定一个有环链表,实现一个算法返回环路的开头结点。
算法思想:
(1)创建两个指针:FastPointer和SlowPointer
(2)SlowPointer每走一步,FastPointer就走两步
(3)两者碰到一起时,将SlowPointer指向有环链表的头结点,FastPointer保持不变。
(4)以相同速度移动SlowPointer和FastPointer,一次一步,新的碰撞点即为环路的开头结点。
public static LinkedListNode FindBeginning(LinkedListNode head) { LinkedListNode slow = head; LinkedListNode fast = head; //找出碰撞处 while (fast != null && fast.next != null) { slow = slow.next; fast = fast.next.next; if (slow == fast) { break; } } //错误检查,没有碰撞处,也即没有环路 if (fast == null || fast.next == null) { return null; } //将slow指向首部,fast不动,两者以相同的速度移动,必定会在环路起始处再次碰撞在一起 slow = head; while (slow != fast) { slow = slow.next; fast = fast.next; } return slow; }
7.编写一个函数,检查链表是否为回文。可以将回文定义为0->1->2->1->0。
思路:只需将链表前半部分反转,可以利用栈将前半部分结点入栈来实现。根据链表长度已知与否,入栈有两种方式。
若链表长度已知,可以用标准for迭代访问前半部分结点,将每个结点入栈。要小心处理长度为奇数的情况。
若链表长度未知,可以用快慢runner方法迭代访问链表。在迭代循环的每一步,将慢速runner的数据入栈。在快速runner抵达链表尾部时,慢速runner刚好位于链表中间位置。
这时,栈里就存放了链表前半部分的所有结点,不过顺序是相反的。接下来,迭代访问链表余下结点。每次迭代时,比较当前结点和栈顶元素,若完成迭代时比较结果完全相同,则该链表是回文序列。
public static boolean isPalindrome(LinkedListNode head) { LinkedListNode fast = head; LinkedListNode slow = head; Stack<Integer> stack = new Stack<Integer>(); //将链表前半部分元素入栈 while (fast != null && fast.next != null) { stack.push(slow.data); slow = slow.next; fast = fast.next.next; } //链表有奇数个元素,跳过中间元素 if (fast.next != null) { slow = slow.next; } while (slow != null) { int top = stack.pop().intValue(); if (top != slow.data) { return false; } slow = slow.next; } return true; }