链表----剑指offer每日6题—第一天
003-从尾到头打印链表
题目描述
输入一个链表,按链表从尾到头的顺序返回一个ArrayList。
思路:推荐使用第一种
- 头插法(非递归):使用ArrayList集合遍历链表进行头插。
单指针
分析:因为链表只能从头到位进行遍历,但是题目却要求从尾到头进行打印,所以可以使用栈的思想来解决。可以使用ArrayList的add(0,value)来实现头插法,也可以使用linkedlist的addFirst(value)即posh(value)和pop()的方法来模拟栈。补充linkedlist:push、pop来模拟栈,offer()、poll来模拟队列。
public ArrayList<Integer> printListFromTailToHead(ListNode listNode) { ArrayList<Integer> list = new ArrayList<>(); while (listNode!=null){ list.add(0,listNode.val); listNode=listNode.next; } return list;
时间复杂度:[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-VyBlOEIE-1595481908937)(https://www.nowcoder.com/equation?tex=O(n)]&preview=true)
空间复杂度:[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-674IAr7t-1595481908940)(https://www.nowcoder.com/equation?tex=O(n)]&preview=true)
- 递归方法:
可以利用递归和回溯的特性来找到最后一个节点,然后倒着放入ArrayList集合。
public ArrayList<Integer> printListFromTailToHead(ListNode listNode) { ArrayList<Integer> list = new ArrayList<>(); return help(list,listNode); } private ArrayList<Integer> help(ArrayList<Integer> list, ListNode listNode) { if (listNode!=null){ help(list,listNode.next); list.add(listNode.val); } return list; }
014-链表中倒数第k个结点
题目描述
输入一个链表,输出该链表中倒数第k个结点。
思路:推荐使用方法1,使用双指针,空间复杂度和时间复杂度都比较好。
双指针
- 因为链表只能从前到后遍历,求倒数第K个也可以理解成用双指针,第一个指针用来遍历,第二个指针等待K步以后再走,那么当第一个指针走到最后的时候第二个指针正好走到了倒数第K个位置。
public ListNode FindKthToTail(ListNode head,int k) { ListNode cur = head; ListNode pre = head; int i=0; while(cur!=null){ if(i>=k) pre=pre.next; cur=cur.next; i++; } return k>i?null:pre; }
- 这个方法空间和时间上都不如第一个方法双指针,记住第一个方法就可以了。
public ListNode FindKthToTail(ListNode head,int k) { if(k<=0) return null; ArrayList<ListNode> list = new ArrayList<>(); ListNode cur = head; while(cur!=null){ list.add(cur); cur=cur.next; } return k>list.size()?null:list.get(list.size()-k); }
015-反转链表
题目描述
输入一个链表,反转链表后,输出新链表的表头。
思路:推荐使用方法1,断头法。
- 使用断头法,遍历链表每次将链表头断掉,形成两个链表。
三指针
public ListNode ReverseList(ListNode head) { ListNode pre= new ListNode(-1); ListNode cur=head; ListNode next=null; while(cur!=null){ next=cur.next; cur.next=pre.next; pre.next=cur; cur=next; } return pre.next; }
016-合并两个或k个有序链表
题目描述
输入两个单调递增的链表,输出两个链表合成后的链表,当然我们需要合成后的链表满足单调不减规则。
思路:推荐使用方法1
- 思路使用两个指针遍历两个链表,然后取出较小的值放入,一个新出有序链表内。
三指针
public ListNode Merge(ListNode list1,ListNode list2) { ListNode pre = new ListNode(-1); ListNode cur = pre; 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 pre.next; }
025-复杂链表的复制
题目描述
输入一个复杂链表(每个节点中有节点值,以及两个指针,一个指向下一个节点,另一个特殊指针random指向一个随机节点),请对此链表进行深拷贝,并返回拷贝后的头结点。(注意,输出结果中请不要返回参数中的节点引用,否则判题程序会直接返回空)
思路:推荐使用方法1,复制链表的精髓是同步遍历,因为此题每个节点有一个随机的索引,可能存在随机索引和next指向的节点重复的问题,所以需要使用一个map来保存每个旧节点和新节点的映射关系,每次复制的时候需要多一个判断是否该需要复制的节点和它映射的新节点都已经存在,如果存在则不需要创建直接获取即可。
- 用两个指针同步指向新链表和旧连表的相同的节点,同步遍历。用map保存旧节点和新节点之间的映射关系,next遍历链表1,判断它的next节点和random节点是否存在map中,如果不存在则新建一个节点复制链表1的节点值然后将映射关系放入map同时放入新链表的next或者random,如果存在则直接获取放入新链表的next或random。
public RandomListNode Clone(RandomListNode pHead) { if(pHead==null) return null; RandomListNode cur1 = pHead; RandomListNode newHead = null; RandomListNode cur2 = null; Map<RandomListNode,RandomListNode> map = new HashMap<>(); while(cur1!=null){ if(cur2==null){ newHead = new RandomListNode(cur1.label); cur2 = newHead; map.put(cur1,cur2); }else{ if(cur1.next!=null&&map.containsKey(cur1.next)){ cur2.next=map.get(cur1.next); }else{ if(cur1.next!=null){ cur2.next=new RandomListNode(cur1.next.label); map.put(cur1.next,cur2.next); } } if(cur1.random!=null&&map.containsKey(cur1.random)){ cur2.random=map.get(cur1.random); }else{ if(cur1.random!=null){ cur2.random=new RandomListNode(cur1.random.label); map.put(cur1.random,cur2.random); } } cur1=cur1.next; cur2=cur2.next; } } return newHead; }
036-两个链表的第一个公共结点
题目描述
输入两个链表,找出它们的第一个公共结点。(注意因为传入数据是链表,所以错误测试数据的提示是用其他方式显示的,保证传入数据是正确的)
思路:注意两个链表第一个公共节点也就是第一个相同节点不仅仅指的是数值相等而且要求节点指向下一个节点的引用相等,也即是公共节点指的是两个节点的地址相等、值相等、next相等。1 2 3 和1 3 4 中1就不是相同的节点。所以我们这里不用hashmap来存储,用一种更巧妙的方法,将链表A和B拼接成A+B和B+A,同步遍历下去,第一个相等的节点即为第一个相等的节点。
public ListNode FindFirstCommonNode(ListNode pHead1, ListNode pHead2) { if(pHead1==null||pHead2==null) return null; ListNode cur1=pHead1; ListNode cur2=pHead2; while(cur1!=cur2){ cur1=cur1.next; cur2=cur2.next; if(cur1!=cur2){ if(cur1==null) cur1=pHead2; if(cur2==null) cur2=pHead1; } } return cur1; }
055-链表中环的入口结点
题目描述
给一个链表,若其中包含环,请找出该链表的环的入口结点,否则,输出null。
思路:用Hashset来装节点,存在则返回true,不存在则返回false;,设置标识位来决定是否结束循环。
public ListNode EntryNodeOfLoop(ListNode pHead)
{
ListNode temp=pHead;
Set set = new HashSet<>();
boolean flag;
while(temp!=null){
flag = set.add(temp);
if(!flag) return temp;
temp=temp.next;
}
return null;
}
056-删除链表中重复的结点
题目描述
在一个排序的链表中,存在重复的结点,请删除该链表中重复的结点,重复的结点不保留,返回链表头指针。 例如,链表1->2->3->3->4->4->5 处理后为 1->2->5
思路:
pre节点用来构建局部新的节点,此时应该分情况考虑, * //若没有进入到处理重复节点,pre和cur一起向下移动,若处理了重复节点,此时pre不需要移动而只移动cur。
public ListNode deleteDuplication(ListNode pHead) { ListNode head = new ListNode(0); ListNode pre = head; pre.next = pHead; ListNode cur = pHead; boolean flag = false;//标志是否出现重复节点 while(cur!=null){ while(cur.next!=null&&cur.val==cur.next.val){ cur=cur.next; flag=true; }//退出循环时,cur指向最后一个重复节点,cur.next指向的是非重复节点 if(!flag){ pre=cur; } cur=cur.next; pre.next=cur; flag=false; } return head.next; }
总结:
- 遇到链表问题就用指针(单指针、双指针、三指针)来解决,因为链表的长度不可知,必须要遍历才能知道,所以可以使用一边遍历一边计数的方式来解决,看情况可以使用单指针或者双指针甚至是三指针来解决问题。
- 还可以引入标志位来决定什么时候退出循环或者局部循环,hashSet和HashMap来判断是否重复。