算法四:链表问题

链表题方法论:

笔试:一般不会要求空间复杂度,因为输入输出本身就很占空间,一切以时间复杂度为主,

面试:因为要以解题思路吸引面试官,所以要顾及到空间复杂度。

解题技巧:

笔试:使用容器(哈希表、数组等),

面试:使用快慢指针(省空间)。

 

链表找中点问题:

面试:

四个问题思路都是快慢指针,但是题目要求不同,导致快慢指针初始值不同,但是接下来的循环遍历是相同的:

while(fast.next != null && fast.next.next != null){ fast = fast.next.next,slow = slow.next;}

1)中点或上中点:慢指针slow = head.next,快指针fast = head.next.next,为什么这样设?因为试验一下,发现是对的。

或慢指针slow = head,快指针fast = head,都行,能实现就行(二者区别是fast和slow是否走了第一步而已)

2)中点或下中点:慢指针slow = head.next,快指针fast = head.next,

2)中点前一个或上中点前一个:慢指针slow = head,快指针fast = head.next.next,

3)中点前一个或下中点前一个:慢指针slow = head,快指针fast = head.next,

笔试:

用什么快慢指针,直接遍历放到ArrayList里,然后穿下标直接获取,

1)取下标(arrayList.size()-1)/2

2)取下标arrayList.size()/2

3)取下标(arrayList.size()-1)/2 - 1即(arrayList.size()-3)/2

4)取下标arrayList.size()/2 - 1即(arrayList.size()-2)/2

 

判断回文:

1)遍历一遍链表,依次入栈,再遍历一遍,跟依次弹栈的(相当于逆序)对比,有一个不一样就返回false,全部一样才返回true。

更省空间的:先找到中点(上一个问题)遍历到中点依次入栈,只要栈不空依次弹栈对比。

2)用上一个问题找到中点,把中点右侧的半部分逆序,逆序完拿一个指针保存一下最后一个节点,因为还要再逆序回去,

依次向中间遍历,发现有不一样的就返回false,完全一样才返回true,

最后通过记录的最后一个节点指针,把右半部分再逆序回去。

 

链表partition:

1)遍历链表得到长度,申请这么长的Node[ ]数组,遍历链表把节点放进数组,然后做荷兰国旗问题。时间复杂度O(N),空间复杂度O(N),

2)省空间,只用6个额外变量:sh(smallHead),st(smallTail),eh(equalHead),et,bh(bigHead),bt,分成了<区,=区,>区,

遍历链表,判断node如果小于基准,就放到<区:如果st == null,则sh = node,st == node;如果st != null,则st.next = node,st = node,

等于和大于同理。

最后把三个链表串起来,要判断每个链表是否为空,否则null.next空指针,

先判断sh是否为空,如果是,sh = eh,如果不是,st要=下一个链表的h,

判断eh是否为空,如果是,st = bh,如果不是,st = eh,et = bh,

返回sh。

 

有任意指针的链表深拷贝:

1)不考虑空间复杂度的话,可以用哈希表:

链表头指针head,创建HashMap<Node,Node>,key是老节点,value是拷贝节点,

按next方向遍历链表,节点cur,令map.put(cur,new Node(cur.value)),即遍历一遍只拷贝节点,把老节点和新节点存到map,不处理指针,

第二遍遍历链表,节点cur,令map.get(cur).next = map.get(cur.next),map.get(cur).rand = map.get(cur.rand),

即老节点get新节点,新节点的next = 老节点的next get 得到老节点的next的拷贝节点,

最后返回map.get(head)。

2)限制空间复杂度O(1):

分三步,第一步在next方向上拷贝链表,第二步拷贝rand指针,第三步在next方向上分离链表,返回新的头节点。

第一次遍历链表,节点cur,拷贝节点curCopy = new Node(cur.value),把curCopy插入到cur下一个,遍历完一遍,就在next方向上拷贝完了,

第二次遍历链表,节点cur,拷贝节点就是cur.next,如果cur.rand == null,那么cur.next.rand = null,否则cur.next.rand = cur.rand.next,

第三次遍历之前,先记录一下Node result = head.next,即记录即将产生的新链表的头节点,到时候直接返回他,

第三次遍历链表,每次遍历一对节点cur和curCopy,先记录下一步环境next = cur.next.next,令curCopy = cur.next,

cur.next = next,如果next == null,则curCopy.next = null,否则cueCopy.next = next.next,

即老节点next=下一个老节点,新节点next=下一个新节点,

遍历完返回result。

3)注意:为什么不能第二遍一边拷贝rand一边分离?

因为如果现在遍历到node3,拷贝完rand把node3和node3Copy分离了,这俩就没关系了,互相找不到了,

加入后面有个node100,rand指向node3,那么node100Copy的rand就要设置指向node3Copy,

而通过node100可以找到node3,但是再也找不到node3Copy了。

 

判断一个单链表中是否有环,求入环的第一个节点:

1)不考虑空间复杂度,用哈希表,很简单:

HashSet<Node>放节点,遍历链表,节点cur,判断哈希表中是否存在set.contains(cur),

如果不存在,就加入set.add(cur),如果已存在,那么当前节点cur就是入环的第一个节点。

2)限制空间复杂度O(1),用快慢指针:

链表头指针head,慢指针slow = head.next,每步走一个节点,快指针fast = head.next.next,每步走两个节点,

因为单链表只有一个next指针,因此如果链表入环,就不可能出环,尾节点连到某节点,遍历永远出不去,

因此如果单链表有环,那么fast和slow必定在环里相遇,因此循环遍历链表,slow每步走一个节点,fast每步走两个节点,

直到slow == fast,二者相遇,

这时让slow还在相遇节点,fast回到head再次遍历,这次fast和slow每次都走一个节点,则二者下次相遇必在第一个入环节点,

只要在slow == fast时返回slow即可。

证明:如图,假设起点为S,入环点为A,在B处第一次相遇,相遇点离入环点走了n,环外长度为m,环周长为c

则第一次相遇:(m + n) / 1 = (m + n) / 2 + k*c,即快指针在多走了k圈之后与慢指针相遇,等式表示时间相等,

化简得到m+n=k*c,

第二次相遇:当fast走了m,到达A时,

slow一开始在B,距离A有n,因为速度一样,slow也走了m,n+m=k*c,即slow离A走了k个整圈,也到达A,

因此这时刚好相遇,即必然在入环处相遇。

 

求两个链表(可能有环也可能无环)相交的第一个节点:

上一个问题判断有环并返回第一个入环节点封装成一个方法Node getloop(Node head),链表head1和head2,

(一)如果两链表都无环,即getloop(head1) == getloop(head2) == null

1)不考虑空间复杂度,用哈希表很简单:

先遍历head1,把节点都加入HashSet<Node>,再遍历head2,判断set中有没有这个节点,如果有,这个节点就是第一个相交节点。

2)限制空间复杂度O(1):

定义int len = 0,遍历head1,len++,遍历完len就是head1的长度,再遍历head2,len--,遍历完len就是两链表长度的差,

令cur1=长链表,cur2=短链表,即如果len>0则cur1=head1,否则cur1 = head2,如果cur1 = head1则cur2=head2,否则cur2=head1,

len取绝对值,循环len次遍历cur1,把长的那部分先遍历完,

(为什么?因为两单链表相交,只可能是Y型或V型,不可能是X型,也就是说相交之后就共用接下来的全部,不会分开,

因此只要长链表来到和短链表等长的位置,同时遍历两链表,就一定能找到相交的节点)

接下来长链表和短链表同时遍历,如果cur1 == cur2就退出,返回cur1就是第一个相交的节点。

(二)两链表都有环,即getloop(head1) != null && getloop(head2) != null

1)入环节点是同一个,即图中(二),那么看作无环链表相交,入环节点看作尾节点,求相交的第一个节点。

2)入环节点不是同一个,得到loop1 = getloop(head1) ,loop2 = getloop(head2),从loop1出发遍历,

如果直到回到loop1也没与loop2相遇,就是图中(一),返回null,

如果中途遇到loop2,就是图中(二),返回loop1和loop2都对,都是第一个相交的节点。

(三)一个有环一个没环

直接返回null,两个单链表相交,有环的话只能都有环且共用环,不可能一个有环一个没环。

 

不给头节点,删除某节点:

答:通过某种方式能在一定程度上达到删除的效果,但是这种方式有很多问题。

怎么删:

如 1—>2—>3—>4,想删2节点,给了n2指向2节点,那么可以得到n3=n2.next,把n3的value全部拷贝给n2,假如n3.copy(n2),

然后删除n3,即n2.next=n3.next,这一招叫借尸还魂,最后也能达到删除2的效果。

有哪些问题:

①如果这些节点不是数而是服务器,不可能把这台服务的信息拷贝到另一台上还能接着跑,

②就算不是服务器,遇到一个复杂的引用类型,无法调用拷贝相关的方法,或者节点是个单例,就没法拷贝,也没法删除,

③这种方法删除不了最后一个节点,因为后面没有节点了,没法拷过来,

也不能令最后一个节点n4=null,因为n4只是节点4的引用,n4去转而指向null了,节点4还呆在原地没有变化,

也不能把最后一个节点析构了就当作null了,null是内存中一块独立的区域,只有令上一个节点的next指向null这块区域才算删除。

因此如果想要准确地删除节点,就必须给出头节点。

删除节点的方法返回值不是void,是Node,返回删除后的链表的头节点,因为有可能换头。

 

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值