1、找出两个链表的第一个公共节点
输入两个链表(例如下面两个链表),两个链表的头节点都是已知的,相交之后成为一个单链表,但是相交的位置未知,并且相交之前的节点数也是未知的,请设计算法找到两个链表的合并点。
解决问题的一个屡试不爽的方法:把常用的数据结构(数组、链表、队、栈、哈希、集合、树、堆)和常用的算法思想(查找、排序、双指针、递归、迭代、分治、贪心、回溯和动态规划等等)想一遍,看看哪些能解决。
首先最简单的是蛮力法,将第一个链表中的每一个节点依次和第二个链表的节点进行对比,当出现相同的节点指针时即为相交点,不过此方法时间复杂度高,不宜使用。
1.1 使用哈希或集合
先将一个链表元素全部存到Map(Set)中,然后一边遍历第二个链表,一边检测Hash中是否存在当前节点,如果有交点那么一定能检测出来,代码如下:
/**
* 通过hash辅助查找
*
* @param head1
* @param head2
* @return
*/
public static ListNode seekFirstCommonNodeByHash(ListNode head1, ListNode head2) {
if (head1 == null || head2 == null) {
return null;
}
ListNode currNode1 = head1;
ListNode currNode2 = head2;
//将第一个链表元素全部存到map中
Map<ListNode, Integer> hashMap = new HashMap<>();
while (currNode1.next != null) {
hashMap.put(currNode1, null);
currNode1 = currNode1.next;
}
//遍历第二个链表
while (currNode2.next != null) {
if (hashMap.containsKey(currNode2)) {
return currNode2;
}
currNode2 = currNode2.next;
}
return null;
}
/**
* 通过集合Set辅助查找
*
* @param head1
* @param head2
* @return
*/
public static ListNode seekFirstCommonNodeBySet(ListNode head1, ListNode head2) {
if (head1 == null || head2 == null) {
return null;
}
Set<ListNode> set = new HashSet<>();
//将第一个链表元素全部存到map中
while (head1.next != null) {
set.add(head1);
head1 = head1.next;
}
//遍历第二个链表
while (head2.next != null) {
if (set.contains(head2)) {
return head2;
} else {
head2 = head2.next;
}
}
return null;
}
/**
* 创建两个链表
*
* @return
*/
public static ListNode[] createLinkedList() {
ListNode[] heads = new ListNode[2];
//构建第一个链表,1 2 3 4 5 6
heads[0] = new ListNode(1);
ListNode currNode1 = heads[0];
currNode1.next = new ListNode(2);
currNode1 = currNode1.next;
currNode1.next = new ListNode(3);
currNode1 = currNode1.next;
//构建第二个链表 8 9 4 5 6
heads[1] = new ListNode(8);
ListNode currNode2 = heads[1];
currNode2.next = new ListNode(9);
currNode2 = currNode2.next;
//构建公共交点以及后面的节点
ListNode commonNode;
commonNode = new ListNode(4);
currNode1.next = commonNode;
currNode2.next = commonNode;
commonNode.next = new ListNode(5);
commonNode = commonNode.next;
commonNode.next = new ListNode(6);
return heads;
}
1.2 使用栈
需要使用两个栈,分别将两个链表的节点入两个栈,然后分别出栈(后进先出),如果相等就继续出栈,一直找到最晚出栈的相等的那一组,这种方法需要两个O(n)的空间,代码如下:
/**
* 通过栈Stack辅助查找
*
* @param head1
* @param head2
* @return
*/
private static ListNode seekFirstCommonNodeByStack(ListNode head1, ListNode head2) {
Stack<ListNode> stackA = new Stack<>();
Stack<ListNode> stackB = new Stack<>();
//分别将两个链表的节点入两个栈
while (head1.next != null) {
stackA.push(head1);
head1 = head1.next;
}
while (head2.next != null) {
stackB.push(head2);
head2 = head2.next;
}
//分别出栈,一直找到最晚出栈的相等的那一组
ListNode listNode = null;
while (stackA.size() > 0 && stackB.size() > 0) {
if (stackA.peek() == stackB.peek()) {
listNode = stackA.pop();
stackB.pop();
} else {
break;
}
}
return listNode;
}
1.3 拼接两个链表
将A链表与B链表互相拼接即变成AB与BA链表,如下
A:1 -> 2 -> 3 -> 4 -> 5
B:9 -> 7 -> 4 -> 5
分别拼接成AB与BA后:
AB:1 -> 2 -> 3 -> 4 -> 5 -> 9 -> 7 -> 4 -> 5
BA:9 -> 7 -> 4 -> 5 -> 1 -> 2 -> 3 -> 4 -> 5
/**
* 通过序列拼接辅助查找
*
* @param head1
* @param head2
* @return
*/
public static ListNode seekFirstCommonNodeByMerge(ListNode head1, ListNode head2) {
if (head1 == null || head2 == null) {
return null;
}
ListNode node1 = head1;
ListNode node2 = head2;
while (node1 != node2) {
node1 = node1.next;
node2 = node2.next;
//读到相等为止
if (node1 != node2) {
//当A遍历完直接将B接到A上,最终成为AB
if (node1 == null) {
node1 = head2;
}
//当B遍历完直接将A接到B上,最终成为BA
if (node2 == null) {
node2 = head1;
}
}
}
return node1;
}
1.4 差和双指针
分别获取到两个链表的长度 |S1-S2| ,让长的先走 |S1-S2|,然后两个链表同时向前走,节点一样的时候就找到公共节点了,代码如下
/**
* 通过差值助查找
*
* @param head1
* @param head2
* @return
*/
public static ListNode seekFirstCommonNodeBySub(ListNode head1, ListNode head2) {
if (head1 == null || head2 == null) {
return null;
}
ListNode node1 = head1;
ListNode node2 = head2;
//分别获取到两个链表的长度
int length1 = 0;
while (node1.next != null) {
node1 = node1.next;
length1++;
}
int length2 = 0;
while (node2.next != null) {
node2 = node2.next;
length2++;
}
int sub = length1 - length2 > 0 ? length1 - length2 : length2 - length1;
//让长的先走
node1 = head1;
node2 = head2;
if (length1 > length2) {
int a = 0;
while (sub > a) {
node1 = node1.next;
a++;
}
}
if (length2 > length1) {
int a = 0;
while (sub > a) {
node2 = node2.next;
a++;
}
}
//两个链表同时向前走,节点一样的时候就找到公共节点了
while (node1 != node2) {
node1 = node1.next;
node2 = node2.next;
}
return node1;
}
2、判断链表是否为回文序列
如输入: 1 -> 2 -> 2 ->1
用O(n)时间复杂度和O(1)空间复杂度解决。
最简单粗暴的方法是将链表元素都赋值到数组中,然后从数组两端向中间对比,但这种方法会被视为逃避链表。
2.1 使用栈判断
将链表元素全部压栈,然后一边出栈一边重新遍历链表(此时相当于是链表头和链表尾部同时向中间进行对比),只要有一个不相等,就不是回文序列,代码如下:
/**
* 将链表元素全部压栈,然后一边出栈一边重新遍历链表,只要有一个不相等,就不是回文序列
*
* @param head
* @return
*/
public static boolean ifPalindromicByStack(ListNode head) {
if (head == null) return true;
ListNode curr = head;
//把链表节点的值存放到栈中
Stack<Integer> stack = new Stack();
int length = 0;
while (curr != null) {
stack.push(curr.val);
curr = curr.next;
length++;
}
//然后再出栈(后进先出) 只比较一半的元素(只出栈一半的元素、链表只遍历一半)
length >>= 1;
while (length-- >= 0) {
if (head.val != stack.pop()) {
return false;
}
head = head.next;
}
return true;
}
2.2 使用递归判断
类似于先将一个链表反转再与原始链表对比
static ListNode temp;
/**
* 通过递归的方法判断
*
* @param head
* @return
*/
public static boolean ifPalindromicByRecursion(ListNode head) {
temp = head;
return checkNode(temp);
}
public static boolean checkNode(ListNode head) {
if (head == null) {
return true;
}
//有一个为false,后面所有的都是返回false
Boolean result = checkNode(head.next) && (head.val == temp.val);
temp = temp.next;
return result;
}
2.3 使用双指针判断
通过双指针思想里面的快慢指针来判断,fast一次走两步,slow一次走一步,当fast到达链表尾部的时候,slow刚好走到一半的位置,然后从头开始逆序一半的元素或者从slow开始逆序一半的元素,代码如下:
/**
* 使用快慢指针判断
*
* @param head
* @return
*/
private static boolean ifPalindromicByTwoPointers(ListNode head) {
//安全性校验
if (head == null && head.next == null) {
return false;
}
ListNode slow = head, fast = head;
ListNode pre = head, prePre = null;
while (fast != null && fast.next != null) {
pre = slow;
slow = slow.next;
fast = fast.next.next;
//获取到slow已经遍历过的逆序的链表
pre.next = prePre;
prePre = pre;
}
//当序列是单数个数时,此时slow里面元素比pre多一个
if (fast != null) {
slow = slow.next;
}
//比较slow与pre
while (pre != null && slow != null) {
if (pre.val != slow.val) {
return false;
}
pre = pre.next;
slow = slow.next;
}
return true;
}