算法面试之链表
- 链表的翻转
- 翻转m到n之间的链表节点
- 复制带随机指针的链表
- 删除链表元素
- 链表的合并
- 带环链表
链表的翻转
public ListNode reverse(ListNode head) {
if(head==null || head.next==null){
return head;
}
ListNode cur=head;
ListNode next=cur.next;
head.next=null;
while(next!=null){
ListNode tmp=next.next;
next.next=cur;
cur=next;
next=tmp;
}
return cur;
}
首先对于链表而言如果链表为空或者链表只有一个节点 则不需要进行处理直接返回head
获取第一个节点命名为cur,获取第二个节点命名为next
进入循环,判断条件为next不等于null
翻转的过程其实就是将next节点的next指针指向cur节点,同时将cur与next节点都向后移动一个位置
但是这样就有一个问题,next节点的后一个位置是通过next.next获得 当next.next节点被赋值为cur时,无法获得next后节点的位置,因此需要先创建临时变量tmp保存next.next节点,之后便是赋值的过程
注意返回的节点时cur节点 因为此时next节点已经为null,cur节点为反转后的头结点。
翻转m到n之间的链表节点
该题与全部链表翻转不同的一点就是需要找到它的起始节点和终止节点
该题有几个易错点:1. 要翻转第m个节点到第n个节点需要获得m-1节点的位置,这个位置对于m=1和m!=1时处理方式不同,为了统一处理我们可以在头结点之前再new一个新的节点,这要保证了m取任意值都能够有前驱节点。 2. 在翻转m到n时,m节点要使用一个指针保存其位置,因为翻转结束后m节点的next指针要连接到n+1节点的位置,如果不保存,则无法将其next指针指向正确位置。
public ListNode reverseBetween(ListNode head, int m, int n) {
if(head==null || head.next==null){
return head;
}
ListNode node=new ListNode(0);//因为翻转m到n节点需要获取m节点的前置节点,对于m=1而言不存在前置节点,因此需要构造一个新节点,这样处理第一个节点与其他节点就完全相同
node.next=head;
head=node;
ListNode reHead=head;
for(int i=0;i<m-1;i++){
reHead=reHead.next;//移动到前置节点
}
ListNode cur=reHead.next;
ListNode first=cur;//因为之后cur节点开始移动,需要保留cur节点在链表移动结束后连接到n+1节点
ListNode next=cur.next;
for(int j=m;j<n;j++){
ListNode tmp=next.next;
next.next=cur;
cur=next;
next=tmp;
}
reHead.next=cur;
first.next=next;
return head.next;
}
- 因为在翻转链表的过程中使用到了当前指针cur,当前指针的next指针next,以及暂时存储的tmp指针next.next,所以这需要保证head!=null 同时head.next!=null
2.在头结点之前构造新的节点,方便对m=1时候的处理,因为最终需要返回head.next 同时需要将指针位置移动到m-1的位置,所以构造一个新的指针reHead用来移动,for循环移动到m-1节点的位置。
3.获取m节点的位置,以及m节点的next节点,之后的循环和翻转链表操作一致
4.最后需要将m到n之间反转后的列表头尾连接到正确位置,即reHead的next指针连接到n个的位置(此时该节点与循环结束cur节点位置一致),保存的first节点连接到n+1节点的位置。
5.返回head.next,因为构造了一个节点,原始的初始节点其实是在head.next位置。
复制带随机指针的链表
带随机指针的链表与只带next指针的链表复制不同的一点是,当只有next指针的时候可以依次构造节点,然后将cur节点的next指针指向next节点,但是具有random指针的时候,我们可以获得random节点,但是我们复制的时候并不能知道random节点实际指向的节点的位置。反映在编程语言上就是
ListNode node=new ListNode(0);
while(head){
ListNode replace=new ListNode(head.val)
node.next=replace;
//node.random=new ListNode(head.random)这样写是有问题的
head=head.next;
node=node.next;
}
因此我们要做的事情就是能够找到新建节点的random指针所对应的节点位置
所以我们要做的就是借助原有的链表构建当前的链表,如果原有链表的random指针与当前链表的random指针之间存在某种next关系,那么我们就可以像构造next指针一样构造random指针了
public RandomListNode copyRandomList(RandomListNode head) {
if(head==null ){
return null;
}
RandomListNode nHead=head;
while(nHead!=null){
RandomListNode node=new RandomListNode(nHead.label);
RandomListNode tmp=nHead.next;//保存next节点位置
nHead.next=node;
node.next=tmp;
nHead=tmp;
}
RandomListNode copyHead=head.next;
RandomListNode mHead=head;
while(copyHead!=null){
if(mHead.random==null){
copyHead.random=null;
}else{
copyHead.random=mHead.random.next;
}
mHead=copyHead.next;
if(copyHead.next!=null){
copyHead.next=copyHead.next.next;
copyHead=copyHead.next;}
else{
copyHead.next=null;
copyHead=copyHead.next;
}
}
return head.next;
}
1.首先新建一个指针指到head节点,head节点保存位置,新建指针进行遍历
2.深拷贝节点值,并把原来节点的next指针指到新建节点上,新建节点的next指针指到原来节点的next指针位置
3.一同遍历原来节点和新建节点,如果原来节点random指针为null则新建节点random指针为null,否则新建节点random指针为原来节点random指针指向的节点的next节点
4.最后因为使用到了copyHead.next.next,如果copy.next指向了null则没有copyHead.next.next,可以做一步额外判断。
5.最终返回head.next
删除链表元素
Remove all elements from a linked list of integers that have value val.
Example
Given 1->2->3->3->4->5->3, val = 3, you should return the list as 1->2->4->5
ublic ListNode removeElements(ListNode head, int val) {
if(head==null){
return null;
}
ListNode node=new ListNode(0);
node.next=head;
head=node;
ListNode cur=head;
ListNode next=head.next;
while(next!=null){
ListNode tmp=next.next;
if(next.val==val){
cur.next=tmp;
next=tmp;
continue;
}
cur=next;
next=tmp;
}
return head.next;
}
1.对于删除来说最重要的也是获得删除节点的前驱节点,因此如果删除的是第一个位置还是会出现不同的处理方式,凡是这样的情况我们都是采用在头结点之前新建一个节点。
2.第二个易错的地方就是当遍历的时候如果next节点位置的val与目标相同则不需要讲cur节点向下移动,因为当前的cur节点依旧是删除节点之后的前驱结点
链表的合并
Merge two sorted (ascending) linked lists and return it as a new sorted list. The new sorted list should be made by splicing together the nodes of the two lists and sorted in ascending order.
Given 1->2->3->3->4->5->3, val = 3, you should return the list as 1->2->4->5
不适用额外的空间
public ListNode mergeTwoLists(ListNode l1, ListNode l2) {
if(l1==null){
return l2;
}
if(l2==null){
return l1;
}
ListNode node=new ListNode(Integer.MIN_VALUE);
node.next=l1;
l1=node;
ListNode cur1=node;
ListNode cur2=l2;
while(cur1!=null&&cur1.next!=null && cur2!=null){//凡是用到cur1.next.next或者cur1.next.val的时候都需要保证cur1.next!=null
if(cur1.val<=cur2.val && cur1.next.val>cur2.val){
ListNode tmp=cur1.next;
ListNode tmp2=cur2.next;
cur1.next=cur2;
cur2.next=tmp;
cur2=tmp2;
cur1=cur1.next;
}
else{
cur1=cur1.next;
}
}
if(cur2==null){
return l1.next;
}else{
cur1.next=cur2;
return l1.next;
}
1.在l1之前增加一个头结点,目的是为了处理l1节点前插节点的需要,因为最后返回值的需要不能直接使用l1做循环遍历,这样就没有指针指向返回的头结点,统一使用cur代指当前节点,next代指下一节点
2.当两个链表都不为null时,如果cur1小于等于cur2的值并且cur1.next的值大于cur2的值进行节点的插入操作,完成后将cur1指针移动到cur2节点处,因为此时cur2该节点编程了cur1的一部分,如果出现相同值不会出错
3.遍历到退出时有两种情况,第一是cur2结束,则直接返回l1.next 另一种是到了cur1的末尾节点,此时cur2中的节点值不可能比cur1的末尾节点值小(如果小,在之前一定已经插入到cur1中) 因此将cur1的next指针指向cur2,返回。