【声明】:由于笔者水平有限,在今后的博文中难免会出现错误和不准之处,本人也非常渴望知道这些错误,恳请读者批评斧正,期待您的留言评论。
唠叨一下:做数据结构题目时不论难易,一定要画图理解,包括后期发布的递归也是这样,千万千万不能懒惰哦!
一、十大链表必刷题有哪些?
1、反转链表 2、找到链表的中间节点 3、找到链表中倒数第k个节点 4、链表分割,所有小于某个数的节点都放在大于或者等于那个数的节点之前 5、删除已经排好序的链表中重复的节点 6、链表的回文结构 7、环形链表I:判断链表是否有环 8、环形链表II:找入口节点 9、相交链表:找到两个链表相交的起始节点 10、合并两个已排好序的链表 11、删除所有出现关键字为key的节点(嘻嘻,你不会真的天真的以为只有十题吧..)
二、算法分析及代码示例
//无头单向非循环链表
class Node {
//实例成员没有初始化,默认值为对应的0值,引用类型默认为null,简单类型默认为0
public int data;//数值域---0
public Node next;//下一个节点的引用---null
public Node(int data) {
this.data = data;
this.next = null;
}
}
1.反转链表(剑指offer)
思路:有两种方法:一是直接翻方向,二是利用头插法,因为该题比较简单,直接看代码示例就能理解,不过在这里,笔者要再强调一遍的是,不管多简单的题目,一定要画图理解,千万不能眼高手低!!!
代码如下(示例):
//1、反转链表
//方法一:直接翻方向(最好掌握这个方法,因为在后面的“回文结构”中也用到了这个方法)
public Node reverseList(Node head) {
Node prev = null;//需要反转的节点的前驱
Node cur = head;//需要反转的节点
Node newHead = null;//反转后的单链表的头
//遍历链表
while(cur != null) {
Node curNext = cur.next;//需要反转的节点的后继
if(curNext == null) {
newHead = cur;
}
cur.next = prev;
prev = cur;
cur = curNext;
}
return newHead;
}
//方法二:头插法(取原链表的节点,头插到新链表)(相对于直接翻方向容易理解一些)
public Node reverseList1(Node head) {
Node cur = head;//进行头插的节点
Node newHead = null;
while(cur != null) {
Node curNext = cur.next;//进行头插的节点的后继
cur.next = newHead;
newHead = cur;
cur = curNext;
}
return newHead;
}
2.找到链表的中间节点
NB(注意):如果有两个中间节点,则返回第二个
思路:利用快慢指针fast和slow,两个都从头开始走,fast一次走两步,slow一次走一步,当fast == null || fast.next == null 时,slow就是中间结点
代码如下(示例):
public Node middleNode(Node head) {
Node slow = head;
Node fast = head;
//只要fast或者fast.next有一个为空,那么肯定走到尾了,注意别忘记加上fast.next!=null
//注意不能写成fast.next != null && fast != null这样,因为fast走两步后可能为空了,再执行
//循环fast.next可能会导致空指针异常
while(fast != null && fast.next != null) {
slow = slow.next;
fast = fast.next.next;
}
return slow;
}
3.找出单链表中倒数第k个节点(剑指offer)
思路:还是利用快慢指针fast和slow,fast先走k-1步,然后fast和slow每个同时走一步,当fast.next==null时,slow就是所求
public Node FindKthToTail(Node head, int k) {
//单链表不能为空
if(head == null) {
return null;
}
//先判断k的合法性
//此处为了只需一次遍历单链表就能解决问题,同时也为了提高时间效率,没有加上k > size()这个
//条件,所以后面要注意空指针异常(第一个while循环做了处理),size()实际上是我写的求单链表
//长度的方法,在此你只要将k > size()理解成"k > 单链表的长度"即可
if(k <= 0) {
System.out.println("k不合法!");
return null;
}
Node slow = head;
Node fast = head;
//不能让它一直往后面走,可能会导致空指针异常
while(k - 1 > 0) {
//设置fast.next != null这个条件就是防止k大于单链表的长度
if(fast.next != null) {
fast = fast.next;
k--;
}else {
System.out.println("没有这个节点!");
return null;
}
}
while(fast.next != null) {
fast = fast.next;
slow = slow.next;
}
return slow;
}
4.链表分割:所有小于x的节点排在大于等于x的节点之前(字节跳动笔试题)
NB:链表分割以后需要保证原来的数据顺序不变-----所以采用“尾插法”
思路:
- 设置两个线段的开始还有结束,分别是bs,be,as,ae
- 定义一个cur,遍历原来的单链表
- 如果cur.data < x,就放到第一个线段,反之,放到第二个线段,注意,为了保证分割链表以后原来的数据顺序不变,此处采用“尾插法”,需要注意的是,一定要判断是不是第一次插入数据,因为第一次和其他次插入数据所执行的操作是不一样的
- cur == null时,原来的单链表就遍历完了
public Node partition(Node head, int x) {
Node bs = null;
Node be = null;
Node as = null;
Node ae = null;
Node cur = head;
while (cur != null) {
if (cur.data < x) {
//第一次插入(bs为空时)
if (bs == null) {
bs = cur;
be = cur;
} else {
be.next = cur;
be = be.next;
}
} else {
//第一次插入(as为空时)
if (as == null) {
as = cur;
ae = cur;
} else {
ae.next = cur;
ae = ae.next;
}
}
cur = cur.next;
}
//1、判断bs是否为空,如果bs == null,返回as
if (bs == null) {
return as;
}
//2、如果bs不为空,需要进行拼接
be.next = as;
//3、如果ae不为空,ae.next需要置为空
if (ae != null) {
ae.next = null;
}
return bs;
}
5.删除已经排好序的链表中重复的节点(字节跳动笔试题)
注意:1、已经排好序的链表就意味着重复的节点集中在一起,读题时就要联想到这一点
2、该题是将重复的节点全部删除干净
思路:定义一个虚拟节点来解决问题(也就是将没有重复出现的节点放到一个新的链表当中)
public Node deleteDuplication(Node head) {
Node cur = head;
Node newHead = new Node(-1);//定义的虚拟节点
Node tmp = newHead;
while(cur != null) {
//一定要加上cur.next != null这个条件,防止一直朝后走导致空指针异常(因为后面要判断
//cur.data == cur.next.data,涉及cur.next,所以要多想一下)
if(cur.next != null && cur.data == cur.next.data) {
while(cur.next != null && cur.data == cur.next.data) {
cur = cur.next;
}
cur = cur.next;//多走一步(因为要把重复的节点全部删除掉)
}else {
tmp.next = cur;
tmp = tmp.next;
cur = cur.next;
}
}
tmp.next = null;//最后一定不要忘记了将尾结点的next置为空
return newHead.next;
}
6.链表的回文结构(腾讯笔试题)
注意:用到了前面的反转链表和找链表的中间节点这两个方法的算法思想
思路:1、找到中间节点
2、翻转链表后半部分的节点
3、先从奇数个节点入手,最后再考虑偶数个节点的情况
public boolean chkPalindrome(Node head) {
//单链表为空,肯定不是回文结构
if(this == null) {
return false;
}
//只有头结点自己,肯定是回文结构
if(head.next == null) {
return true;
}
//1、找到单链表的中间节点
Node fast = head;
Node slow = head;
while(fast != null && fast.next != null) {
fast = fast.next.next;
slow = slow.next;
}
//2、开始反转单链表的后半部分 slow肯定在中间位置
Node cur = slow.next;
while(cur != null) {
Node curNext = cur.next;
cur.next = slow;
slow = cur;
cur = curNext;
}
//slow是最后的节点了
//3、开始一个从头走,一个从尾走
while(slow != head) {
if(slow.data != head.data) {
return false;
}
//判断偶数的情况(一定要注意的是不能比较的是元素大小!!!)
if(head.next == slow) {
return true;
}
slow = slow.next;
head = head.next;
}
return true;
}
7.环形链表I:判断链表是否有环(百度笔试题)
思路:本题很简单,利用快慢指针做即可
public boolean hasCycle(Node head) {
Node fast = head;
Node slow = head;
//如果有环,必然不会存在null
while(fast != null && fast.next != null) {
fast = fast.next.next;
slow = slow.next;
if(fast == slow) {
return true;
}
}
return false;
}
8.环形链表II:找入口节点(腾讯笔试题)
思路:1、设环的长度为C,相遇时fast走了N圈,从头到入口点的距离为X,入口点到相遇点为Y;
2、如果有环,fast和slow相遇的时候,fast走的路程是slow的两倍,2 * (X + Y) = X + NC + Y----->X + Y = NC----->X = NC - Y
3、NC是一个定值,当N == 1时,相遇点到入口点的距离就是X,此时使用两个引用,一个 从头开始走,一个从相遇点开始走,当二者相遇的时候就是入口点
public Node detectCycle(Node head) {
Node fast = head;
Node slow = head;
//如果有环,必然不会存在null
while(fast != null && fast.next != null) {
fast = fast.next.next;
slow = slow.next;
if(fast == slow) {
break;
}
}
if(fast == null || fast.next == null) {
return null;
}
//相遇后,即有环,此时使用两个引用,一个从头开始走,一个从相遇点(此时的slow和fast就在相
//遇点)开始走,当二者相遇的时候就是入口点
slow = head;
while(slow != fast) {
slow = slow.next;
fast = fast.next;
}
return slow;
}
9.相交链表(剑指offer)
思路:1、求两条单链表的长度
2、计算两条单链表的差值
3、让长的链表先走差值步,后面详细步骤见代码块即可求出两链表第一次相交节点
public Node getIntersectionNode(Node headA, Node headB) {
//1、求长度,走差值步
int lenA = 0;
int lenB = 0;
Node pl = headA;
Node ps = headB;
while(pl != null) {
lenA++;
pl = pl.next;
}
while(ps != null) {
lenB++;
ps = ps.next;
}
//注意此时pl,ps位置不再是头了,需要人为设置
pl = headA;
ps = headB;
//len要放到这个位置计算,最基本的都忘记了!!!
int len = lenA - lenB;
//保证pl里面放的是长链表,ps里面放的是短链表
if(len < 0) {
pl = headB;
ps = headA;
len = lenB - lenA;
}
//长的链表走差步值
for(int i = 0; i < len; i++) {
pl = pl.next;
}
//2、ps和pl一定在同一个起跑线上
//设置ps != pl这个条件就是为了相交的时候停下来
//别忘了pl != null && ps !=null 这个条件,其实也可以省去,因为两链表剩下的节点一样长!
while(ps != pl && pl != null && ps != null) {
ps = ps.next;
pl = pl.next;
}
//二者相等也可能是两条链表都为空了
if(pl == ps && pl != null) {
return pl;
}
return null;
}
10.合并两个有序的链表(剑指offer)
思路:利用虚拟节点即可
public Node mergeTwoLists(Node headA, Node headB) {
//开辟一个虚拟节点
Node newHead = new Node(-1);
Node tmp = newHead;
while(headA != null && headB != null) {
if(headA.data < headB.data) {
tmp.next = headA;
headA = headA.next;
tmp = tmp.next;
}else {
tmp.next = headB;
headB = headB.next;
tmp = tmp.next;
}
}
if(headA != null) {
tmp.next = headA;
}
if(headB != null) {
tmp.next = headB;
}
return newHead.next;
}
11.删除所有关键字为key的节点(剑指offer)
思路:本题比较简单,但是最后别忘记了要回过头来判断第一个节点
public void removeAllKey(Node head, int key) {
//单链表为空时
if(head == null) {
return;
}
//需要注意的是始终要保证prev是cur的前驱
Node prev = head;
Node cur = head.next;//代表要删除的节点
while(cur != null) {
if(cur.data == key) {
prev.next = cur.next;
cur = cur.next;
}else {
prev = cur;
cur = cur.next;
}
}
//最后再判断首节点
if(head.data == key) {
head = head.next;
}
}
总结
与其说它是十大链表必刷题,不如把“十大”改成“十类”,因为LeetCode和牛客网等刷题网站上面至少80%的题目都是这十类题目本身,或是其变形形式,中心算法思想是一致的,稍稍改动即可,不过若要会做变形形式,首先需要将这些最基本的原题烂熟于心,数据结构的重要性不用说大家都明白,链表在数据结构中也是举足轻重的。
可能我们在听老师讲课的时候,或者就拿上面的代码举例吧,我们听或者看的时候,能听懂也能看懂,但是到自己实现起来就不是那么一回事了,笔者本人也是这样,究其原因呢,还是题目刷少了,代码量跟不上!所以平时要多练习,笔者自己呢,也是刚刚开始刷LeetCode没多长时间,希望与大家共同进步。
此处需要敲黑板——若要真正掌握上面的题目,我们最起码要练习五遍以上,而且每一遍都要画图理解,包括之后即将发布的递归也是这样,我们处在一个非常美好的时代,时不我待,万万不能懒惰呀铁子们,我自己也是!
还有还有,感谢铁子们能看到最后,蟹蟹啦!