2020.05.31
1、链表倒序输出为数组
输入一个链表,按链表从尾到头的顺序返回一个ArrayList。
思路1:直接遍历链表,每次插入到0
的位置,但是这样数组后面的元素都需要移动,效率低。
思路2:使用栈,先全放入栈,再输出
注意:因为不能改变输入的链表,所以不能将链表倒序再输出。
public ArrayList<Integer> printListFromTailToHead(ListNode listNode) {
ArrayList<Integer> res=new ArrayList<>();
if (listNode==null){
return res;
}
ArrayDeque<ListNode> stack=new ArrayDeque<>();
//1、依次遍历存入栈
while (listNode!=null){
stack.push(listNode);
listNode=listNode.next;
}
//2、从栈弹出结算
while (!stack.isEmpty()){
res.add(stack.pop().val);
}
return res;
}
2、输入一个链表,输出该链表中倒数第k个结点
思路:这题思路不难,但是需要注意处理k和链表长度的关系
使用快慢指针,快指针先走k,然后和慢指针一起走,快指针走到了结尾(null),则慢指针所指向的结点就是倒数第个结点。相当于将倒数第k的长度k转移到顺数第k
/**
* 快慢指针的应用:关键是处理好k和链表长度的关系
* @param head
* @param k
* @return
*/
public static ListNode FindKthToTail(ListNode head,int k) {
if (head==null||k<=0){
return null;
}
ListNode fast=head,slow=head;
//鲁棒性:处理k的大小关系
while (fast!=null&&k>0){
fast=fast.next;
k--;
}
//处理k>长度的情况,鲁棒性
if (k>0){
return null;
}
while (fast!=null){
slow=slow.next;
fast=fast.next;
}
return slow;
}
扩展:寻找链表的中间结点
3、链表反转
输入一个链表,反转链表后,输出新链表的表头。
思路:这题是震哥字节一面的算法题。
(1)使用cur结点依次遍历链表
(1)使用next结点保存下一个结点,这样才能成功遍历到下一个
(2)使用pre结点表示前一个,这样cur.next=pre就完成了逆序。而且因为第一次循环的cur=head,cur.next=pre,所以其实pre就是需要返回的结果链表的头结点。
/**
* @param head
* @return
*/
public ListNode Reverse(ListNode head) {
if (head == null) {
return null;
}
ListNode pre=null,cur=head,next=null;
while (cur!=null){
//1、先报错下一个结点
next=cur.next;
//2、实现逆序
cur.next=pre;
//3、后面的两步都是为了能够继续遍历链表
pre=cur;
cur=next;
}
//4、pre就是结果链表的头结点
return pre;
}
4、外排法合并链表
输入两个单调递增的链表,输出两个链表合成后的链表,当然我们需要合成后的链表满足单调不减规则。
思路:就是归并排序的链表形式,其实比归并排序更加简单。如果数数组的归并排序,还需要在merge函数那里使用辅助数组,先merge到辅助数组,然后再还原到arr。链表的merge只需要引入一个结果结点res,然后cur=res,使用cur去结算结果。
public static ListNode Merge(ListNode list1, ListNode list2) {
if (list1 == null) {
return list2;
}
if (list2==null){
return list1;
}
ListNode res=new ListNode(-1);
//注意不是res.next,因为这时res.next=null
ListNode cur=res;
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;
}
//因为是链表,所以不需要使用while循环
if (list1!=null){
cur.next=list1;
}
if (list2!=null){
cur.next=list2;
}
return res.next;
}
2020.06.01
5、复制复杂链表(链表类型题目的boss)
输入一个复杂链表(每个节点中有节点值,以及两个指针,一个指向下一个节点,另一个特殊指针random指向一个随机节点),请对此链表进行深拷贝,并返回拷贝后的头结点。(注意,输出结果中请不要返回参数中的节点引用,否则判题程序会直接返回空)
关键是需要根据random指针找到随机结点,能够完成random的构建。
思路1:使用hashmap:hash存的key是每个节点、value是复制节点,这样就能很快找到复制节点,再处理指针
(1)遍历链表,依次存入map,key就是cur结点,value是以cur.val生成的复制结点
(2)再次遍历链表,从map中获取当前结点的对应的value(也就是复制结点)复制结点的next就是map中从cur.next中获取,map.get(cur).next = map.get(cur.next);
同理随机指针也是这样操作:map.get(cur).random = map.get(cur.random);
/**
* 使用hash表实现,比较简单
* @param head
* @return
*/
public static Node copyListWithRand1(Node head) {
HashMap<Node, Node> map = new HashMap<Node, Node>();
Node cur = head;
//1.遍历链表,将节点放入hash表,value为新产生的节点
while (cur != null) {
map.put(cur, new Node(cur.value));
cur = cur.next;
}
//2.重新回到头结点,遍历链表
cur = head;
while (cur != null) {
//cur的value也就是复制的节点的next为cur.next对应的value
map.get(cur).next = map.get(cur.next);
//随机节点也是一样处理
map.get(cur).rand = map.get(cur.rand);
cur = cur.next;
}
//3、返回头结点对应的value(也就是头结点对应的复制节点)
return map.get(head);
}
思路2:其实也可以将random指针的关系记录到链表本身,将复制结点cloneNode放在当前结点cur的next,这样,cloneNode.next=cur.next.next;同时也可以通过cloneNode.random=cur.random.next找到随机结点但是需要进行拆分处理,将复杂的问题拆分成小的问题再分别处理。
public RandomListNode Clone(RandomListNode pHead) {
if (pHead == null) {
return null;
}
//1、遍历复制结点
RandomListNode curNode = pHead, cloneNode = null, nextNode = null;
while (curNode != null) {
cloneNode = new RandomListNode(curNode.label);
nextNode = curNode.next;
//挂链
curNode.next = cloneNode;
cloneNode.next = nextNode;
curNode = nextNode;
}
//2、处理随机结点
curNode = pHead;
while (curNode != null) {
cloneNode = curNode.next;
//注意什么时候都需要进行判断是否为空指针
if (curNode.random != null) {
cloneNode.random = curNode.random.next;
}
//注意这里因为有了复制结点,需要两次next以后才是next结点
curNode = curNode.next.next;
}
//3、拆分:只需按照next来拆--最难
curNode = pHead;
RandomListNode res = curNode.next;
while (curNode != null) {
cloneNode = curNode.next;
//拆分原链表:下面是不需要判断的,因为如果curNode!=null,则curNode.next肯定不为null
//因为实际上就是复制结点
// if (curNode.next!=null){
nextNode = curNode.next.next;
curNode.next = nextNode;
//拆分复制链表
if (cloneNode.next != null) {
cloneNode.next = cloneNode.next.next;
}
curNode = nextNode;
}
return res;
}
6、找出它们的第一个公共结点
输入两个链表,找出它们的第一个公共结点。(注意因为传入数据是链表,所以错误测试数据的提示是用其他方式显示的,保证传入数据是正确的)
思路:可以使用栈,两个链表先入栈,然后依次弹栈,找到最后一个相同的结点,这个结点就是第一个公共结点,因为如果链表相交,显然最后结点一样的,但是这样需要辅助空间,显然不是最优解。
最优解:使用四个变量,分别记录pHead1的end1,len1;pHead2的end2,len2;(避免了使用栈的辅助空间)
(1)如果end1!=end2,则肯定没有相交的结点
(2)如果end1==end2,则先算出len1和len2的差值num,长的链表先走num,然后在一起走,遇到相等的,那就是第一个相交结点
public ListNode FindFirstCommonNode(ListNode pHead1, ListNode pHead2) {
if (pHead1 == null || pHead2 == null) {
return null;
}
int len1 = 0, len2 = 0;
ListNode end1 = pHead1, end2 = pHead2;
while (end1.next != null) {
end1 = end1.next;
len1++;
}
while (end2.next != null) {
end2 = end2.next;
len2++;
}
//结尾不同的话肯定没有公共节点
if (end1 != end2) {
return null;
}
int more = 0;
//end1指向长的
if (len1 >= len2) {
more = len1 - len2;
end1 = pHead1;
end2 = pHead2;
} else {
more = len2 - len1;
end1 = pHead2;
end2 = pHead1;
}
//长的先走
for (int i = 0; i < more; i++) {
end1 = end1.next;
}
//再一起走
while (end1 != end2) {
end1 = end1.next;
end2 = end2.next;
}
return end1;
}
7、找出链表的入环节点
给一个链表,若其中包含环,请找出该链表的环的入口结点,否则,输出null。
思路:使用快慢指针,快指针每次走2步,慢指针每次走1步,两个指针相遇的时候一定是在环内,相遇以后快指针回到起点(慢指针保持不动),然后快慢指针一起走,再次相遇的结点就是入环结点。
public ListNode EntryNodeOfLoop(ListNode pHead) {
if (pHead==null){
return null;
}
ListNode fast=pHead,slow=pHead;
//1、找到相交结点
while (fast!=null&&fast.next!=null){
fast=fast.next.next;
slow=slow.next;
if (slow==fast){
break;
}
}
//2、说明是因为没有环退出循环的
if (fast==null||fast.next==null){
return null;
}
//3、fast回到起点,slow不动
fast=pHead;
while (fast!=slow){
fast=fast.next;
slow=slow.next;
}
return slow;
}
扩展:两个单链表相交的一系列问题(找到两个有环单链表的第一个相交结点)
问题1:判断链表是否有环?有环的话返回入环节点,否则返回null
问题2:找出两个无环链表的第一个相交结点
如果一个链表有环一个无环,不可能相交,因为链表是线性结构。
问题3:两个有环链表相交的第一个结点。
package com.iyuanyuan.list;
/**
* 〈一句话功能简述〉
* 〈两个单链表相交的一系列问题〉
*
* @author wenjun
* @create 2020/6/1
* @since 1.0.0
*/
public class FindFirstIntersectNode {
/**
* 链表节点
*/
public static class Node {
public int value;
public Node next;
public Node(int data) {
this.value = data;
}
}
/**
* 主要过程函数
* @param head1
* @param head2
* @return
*/
public static Node getIntersectNode(Node head1, Node head2) {
if (head1==null||head2==null){
return null;
}
Node loop1 = getLoopNode(head1);
Node loop2 = getLoopNode(head2);
//单链表相交问题
if (loop1==null&&loop2==null){
return noLoop(head1,head2);
}else if (loop1!=null&&loop2!=null){
return bothLoop(head1,loop1,head2,loop2);
}else {
//不可能存在一个有环一个无环
return null;
}
}
/**
* 1、判断是否有环
* @param head
* @return
*/
public static Node getLoopNode(Node head) {
if (head==null){
return null;
}
Node fast=head,slow=fast;
while (fast!=null&&fast.next!=null){
fast=fast.next.next;
slow=slow.next;
if (slow==fast){
break;
}
}
if (fast.next==null){
return null;
}
fast=head;
while (fast!=slow){
fast=fast.next;
slow=slow.next;
}
return slow;
}
/**
* 2、无环单链表第一个相交结点
* @param head1
* @param head2
* @return
*/
public static Node noLoop(Node head1, Node head2) {
if (head1==null||head2==null){
return null;
}
Node end1=head1,end2=head2;
int len1=0,len2=0,d=0;
while (end1.next!=null){
end1=end1.next;
len1++;
}
while (end2.next!=null){
end2=end2.next;
len2++;
}
if (len1>=len2){
end1=head1;
end2=head2;
d=len1-len2;
}else {
end1=head2;
end2=head1;
d=len2-len1;
}
for (int i=0;i<d;i++){
end1=end1.next;
}
while (end1!=end2){
end1=end1.next;
end2=end2.next;
}
return end1;
}
/**
* 3、找到两个有环链表相交的第一个结点
* @param head1
* @param loop1
* @param head2
* @param loop2
* @return
*/
public static Node bothLoop(Node head1, Node loop1, Node head2, Node loop2) {
if (head1==null||loop1==null||head2==null||loop2==null){
return null;
}
if (loop1==loop2){
//1、说明是情况2,但是不能直接调用上面的noLoop,因为结束的条件不一样
Node end1=head1,end2=head2;
int len1=0,len2=0,d=0;
while (end1.next!=loop1){
end1=end1.next;
len1++;
}
while (end2.next!=loop2){
end2=end2.next;
len2++;
}
if (len1>=len2){
end1=head1;
end2=head2;
d=len1-len2;
}else {
end1=head2;
end2=head1;
d=len2-len1;
}
for (int i=0;i<d;i++){
end1=end1.next;
}
while (end1!=end2){
end1=end1.next;
end2=end2.next;
}
return end1;
}else {
//2、说明是case1或者case3
Node cur=loop1.next;
while (cur!=loop1){
//需要先判断再遍历下一个,这样才不会错过第一个
if (cur==loop2){
//说明找到了loop1和loop2的相等的,也就是case3
return loop1;
}
cur=cur.next;
}
//退出循环说明走了一圈还没找到loop2,则是case1,没有相交
return null;
}
}
public static void main(String[] args) {
// 1->2->3->4->5->6->7->null
Node head1 = new Node(1);
head1.next = new Node(2);
head1.next.next = new Node(3);
head1.next.next.next = new Node(4);
head1.next.next.next.next = new Node(5);
head1.next.next.next.next.next = new Node(6);
head1.next.next.next.next.next.next = new Node(7);
// 0->9->8->6->7->null
Node head2 = new Node(0);
head2.next = new Node(9);
head2.next.next = new Node(8);
head2.next.next.next = head1.next.next.next.next.next; // 8->6
System.out.println(getIntersectNode(head1, head2).value);
// 1->2->3->4->5->6->7->4...
head1 = new Node(1);
head1.next = new Node(2);
head1.next.next = new Node(3);
head1.next.next.next = new Node(4);
head1.next.next.next.next = new Node(5);
head1.next.next.next.next.next = new Node(6);
head1.next.next.next.next.next.next = new Node(7);
head1.next.next.next.next.next.next = head1.next.next.next; // 7->4
// 0->9->8->2...
head2 = new Node(0);
head2.next = new Node(9);
head2.next.next = new Node(8);
head2.next.next.next = head1.next; // 8->2
System.out.println(getIntersectNode(head1, head2).value);
// 0->9->8->6->4->5->6..
head2 = new Node(0);
head2.next = new Node(9);
head2.next.next = new Node(8);
head2.next.next.next = head1.next.next.next.next.next; // 8->6
System.out.println(getIntersectNode(head1, head2).value);
}
}
8、有序链表删除重复节点
在一个排序的链表中,存在重复的结点,请删除该链表中重复的结点,重复的结点不保留,返回链表头指针。 例如,链表1->2->3->3->4->4->5 处理后为 1->2->5
思路:在遍历的同时判断是否为重复节点,则删除,关键是需要记录前一个结点
(1)因为第一个结点就有可能重复,需要被删除,所以需要先新建一个结点res,res.next=pHead,返回结果就是res.next,而不是新建结点直接指向pHead
(2)使用pre记录重复结点的前一个结点,这样才能保证正确钩链;使用cur遍历链表,如果遇到cur.val=cur.next.var,则使用while循环,一直遍历到cur.val!=cur.next.val,此时的cur就是最后一个重复结点,还需要cur=cur.next,这样就找到了第一个不重复的结点,然后令pre.next=cur;如果一开始就遇到cur.val!=cur.next.var,则直接pre=cur,cur=cur.next,继续遍历下一个
(3)最后返回res.next就是结果
需要注意的问题:
1、比较节点重复应该是比较数值,而不是对象引用;
2、如何将新的链表返回?需要新建res结点,不能直接使用pHead(因为可能被删除)
public class DeleteDuplication {
public ListNode deleteDuplication(ListNode pHead){
if (pHead==null){
return null;
}
//开始遍历
ListNode res=new ListNode(-1);
res.next=pHead;
ListNode pre=res,cur=pHead;
while (cur!=null){
//1、找到重复的结点
if (cur.next!=null&&cur.val==cur.next.val){
//需要一直找下去,找到最后一个重复结点
while (cur.next!=null&&cur.val==cur.next.val){
cur=cur.next;
}
//pre连接后一个结点
cur=cur.next;
pre.next=cur;
}else {
//2、没有重复的则直接往后遍历
pre=cur;
cur=cur.next;
}
}
return res.next;
}
}