没啥说的,列出来下面要写啥:
- 链表倒数第K个结点
- 合并两个有序链表
- 逆序打印链表
- 判断两个链表是否相交
- 判断链表是否有环
1、找出单链表倒数第K个结点
如果是一个顺序表,这道题的处理可以是逆序遍历到第K个就可以了。
很可惜,这道题要是双向链表,借助pre
,也可以这么做;可惜换不得。
单链表只有next
域,是单向的,所以我们只能顺序遍历。
思路: 这道题最基础的方法,是我们得到链表的长度 len,那么倒数第K个不就是 len - k个。
图解:
(这块应该说明一下,没有第0个)
Java中的链表LinkedList
(双向的哦),是维护了一个size()
方法的;即使没有,在给定头节点的情况下,我们也可以遍历链表、得到它的长度。
方法一:
代码:
public Node<T> findVal(Node<T> node, int k) {
if (k <= 0 || node == null) return null;
//倒数第 k 个不就是 list.getLength() - k
int len = getLen(node);
int index = len - k;
Node<T> temp = theFirst;
//从头到尾遍历
while (temp != null && index > 0) {
temp = temp.next;
index--;
}
if (index <= 0) {
return temp;
}
return null;
}
private int getLen(Node<T> node) {
int len = 0;
while (node != null) {
node = node.next;
len = len + 1;
}
return len;
}
其实在遍历的时候,我相信有的人就会有疑问,该是node != null
还是node.next != null
,其实都可以,我是在不确定的情况下就数一数:
node != null
node.next != null
这个也是一样的:
这个虽然整体的时间复杂读是O(N),但它需要遍历两次链表
方法二:
只需要遍历一次链表,牵扯一点点的数学知识
思路: 就是设置两个东西p1、p2,就叫它们指针(Java无此概念,好用尔)吧。
让p1
先走K - 1步,再让两个指针一起走,直至遍历到链表尾,这时p2
恰好会走到倒数第K个!
图解:
public Node<T> findVal(Node<T> node, int k) {
if (k < 1 || node == null) return null;
Node<T> p1 = node;
Node<T> p2 = node;
//让快指针(快引用)成功的先走 k - 1步
for (int i = 0; i < k - 1; i++) {
if (p1.next != null) {
p1 = p1.next;
} else {
return null;
}
}
//再让快慢指针一起走,当快指针走到链表尾,慢指针就走到了倒数第k个
while (p1.next != null) {
p1 = p1.next;
p2 = p2.next;
}
return p2;
}
至于这块的 p != null
还是p.next != null
,我就不说了,上面已经说了个方法,留给读者自己去揣摩。
2、合并两个有序链表
看到这个题啊,又想起刚开始学数据结构的时候,老师讲这个东西,就像是一场梦,醒来还是不敢动~
那时候链表都搞不明白,不会合并,还TM有序合并。。。
现在不一样了,现在会百度啦!!!
思路:
先new
新的链表头,然后我们可以比较两个链表的头,哪个小,哪个就是新链表的头;
然后同时遍历两个链表,比较,将小的结点的值链到新链表的next域,直到有一个链表遍历完
这个时候会剩下一个链表,那么这个链表的值肯定都大于新的链表的值,你品品?
图解:
代码:
public Node<T> mergeLink(Node<T> node1, Node<T> node2) {
if (node1 == null) return node2;
if (node2 == null) return node1;
Node<T> newHead = null;
//找出两个链表中最小的
//比较两个链表的第一个,哪个小,就是新链表的头
if (node1.data <= node2.data) {
newHead = node1;
node1 = node1.next;
} else {
newHead = node2;
node2 = node2.next;
}
//然后while循环比较剩下的元素
//最后返回 newNode,所以让tmp代替它跑
Node<T> tmp = newHead;
while (node1 != null && node2 != null) {
if (node1.data <= node2.data) {
tmp.next = node1;
node1 = node1.next;
tmp = tmp.next;
} else {
tmp.next = node2;
node2 = node2.next;
tmp = tmp.next;
}
}
//比较完还有两种可能,就像归并排序那里
//有可能链表1没比较完,有可能链表2没比较完
if (node1 == null) {
//如果node1跑完了,
tmp.next = node2;
} else {
//node2跑完了
tmp.next = node1;
}
return newHead;
}
3、逆序打印链表
思路:
方法1,可以借助栈。栈,先进后出、后进先出,等到链表所有元素入栈,再依次出栈;
方法2,递归,在输出这个结点之前,我们可以先去打印这个结点的下一个
方法1:
public void reversePrint(Node<T> node) {
Stack<Node<T>> stack = new Stack<>();
Node<T> tmp = node;
while (tmp != null) {
stack.push(tmp);
tmp = tmp.next;
}
while (!stack.isEmpty()) {
System.out.print(stack.pop().data + " ");
}
System.out.println();
}
方法2:
public void reversePrint(Node<T> node) {
if (node!= null) {
reversePrint(node.next);
System.out.print(node.data + " ");
}
System.out.println();
}
递归调用过程:
画的有点乱,我不会搞动图。
红色的线代表递归调用步骤,蓝色的线代表退出递归。
4、两个链表是否相交
思路: 可以用暴力搜索,去判断每个结点是否相等。这个就不写了
另一种思路就是我从书上看到的:如果两个链表相交,那么它们就有着相同的尾结点。
一开始我也不太理解,难道不相交,恰好相等了,怎么办?
其实就是书上说的,相交尾结点就是相等的。
我们知道,链表的结点类型在Java里面也就是一种类 类型,对于每一个结点都是new
出来的。会在堆上开辟出来一块空间,供这个结点使用。
那么不同结点的堆信息肯定不同。
相等,就说明地址相同,它们是同一个结点。
则遍历两个链表,比较尾结点就OK
public boolean isIntersect(Node<T> node1, Node<T> node2) {
if (node1 == null || node2 == null) return false;
Node<T> tmp1 = node1;
Node<T> tmp2 = node2;
while (tmp1.next != null) tmp1 = tmp1.next;
while (tmp2.next != null) tmp2 = tmp2.next;
return tmp1 == tmp2;
}
两个链表相交后,相交的第一个结点?
思路: 如果有两个货相交,总存在node1 == node2
那我们要去找到这两个相等的结点,观察上面辣个图,会发现这相等的结点是在node1的第四个,node2的第三个
也就是,node1走一步,这个相等结点就是node1跟node2一起再走三步
也就是,让长的链表先走,先走多少 -> 与短链表的差
然后两个链表一起走,相等就找到了
代码:
//两个链表相交,找到它们相交的第一个结点
public Node<T> getFirstMeetNode(Node<T> node1, Node<T> node2) {
//如果两个链表不相交
if (!isIntersect(node1, node2) || node1 == null || node2 == null) return null;
int len1 = 1;//咳咳
int len2 = 1;
Node<T> tmp1 = node1;
Node<T> tmp2 = node2;
//遍历出两个链表的长度
while (tmp1.next != null) {
len1++;
tmp1 = tmp1.next;
}
while (tmp2.next != null) {
len2++;
tmp2 = tmp2.next;
}
//求出这个长度
//不知道 len1 大还是 len2 大
//求出绝对值
int dif = Math.abs(len1 - len2);
//如果 len1 大,链表1先走
if (len1 > len2) {
while (dif > 0) {
node1 = node1.next;
dif--;
}
} else {
//len2 大,链表2先走
while (dif > 0) {
node2 = node2.next;
dif--;
}
}
//让两个链表一起走,相等就是第一个相交的结点
while (node1 != node2) {
node1 = node1.next;
node2 = node2.next;
}
return node1;//return node2
}
5、链表是否有环
思路: 链表有环是怎样遍历的呢?
它的node.next != null
是一直成立的,会在圈里打转。
所以这个题目,设置一个快指针,设置一个慢指针。
快指针、慢指针一起走,快指针一次走两步,慢指针一次走一步。
不出意外的话,快指针总能追得上慢指针、fast == slow
,说明有环。
如果没环的话,那么慢指针是追不上快指针的、更别指望快指针追上慢指针。
代码:
//链表是否有环
public boolean isLoop(Node<T> node) {
if (node == null) return false;
Node<T> fast = node;
Node<T> slow = node;
//快指针一次走两步,
while (fast != null && fast.next != null) {
fast = fast.next.next;
slow = slow.next;
if (fast == slow) return true;
}
return false;
}
若链表有环,环的入口节点?
思路: 这个其实是比较涉及到我的知识盲区的
书上的这段话,让我看出来
如果fast
一次两步, slow
一次一步,最终相遇后
从链表头到环入口的距离L2 == 环入口到相遇点的距离L1
也就是,让 fast 继续在相遇点往下遍历,让 slow 从头遍历,当它们相等时,就是环的入口节点。 (这时其实没有slow、fast之分,它们每次走一步)
代码:
在上面代码的基础上
public Node<T> findLoopPort(Node<T> node) {
if (node == null) return null;
Node<T> fast = node;
Node<T> slow = node;
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 = node;
while (slow != fast) {
slow = slow.next;
fast = fast.next;
}
return slow;
}