参考:
左神算法课(大力推荐,感兴趣的同学都可以自己去一探究竟)
注意:本篇文章的代码并不完全正确性,主要是解题思路的分享
1. 结题思路总结
高屋建瓴,面对链表问题我们可以尝试从下面这些角度去考虑可能的解答方法
-
- 快慢指针的灵活灵活运用
-
- 用额外的数据结构来记录链表的结构以及必要的信息
-
- 大胆调整链表的结构并完成恢复
下面我们用具体的例题来说明个种方法的具体运用
2. 回文串判断
题目说明:回文串的概念是指:一个链表无论是从前往后读还是从后往前读其结果都是相同的。例如1->2->3->2->1,又如1->2->2->3。
2.1 思路一:基于栈数据结构
基于栈的思路其实是非常的浅显易懂的:如果是回文串,容易想见链表一定存在一个可以对折的中间线(见下图)。只需要将链表中的元素一个一个加入栈中,当来到中间线之后,对接下来每一个想要入栈的元素进行判断:如果该元素与栈顶元素相同,那么将栈顶元素弹出,如此反复。如果最后栈为空,那么是回文串,否则不是回文串。
注意:这个方法存在一个问题,为了找到中间线,我们势必要定位到该链表的中间节点,一个朴素的方案是直接先确定链表的长度
public static boolean isRoundList_S(LinkedList<Integer> list) {
Stack<Integer> stack = new Stack<>();
int len = list.size();
boolean even = (len % 2) == 0; // 判断链表中的元素个数是否是偶数个
for (int i = 0; i < len; ++i) {
if (i < len / 2) {
stack.push(list.get(i));
} else if (even) {
if (!Objects.equals(stack.pop(), list.get(i))) return false;
} else {
even = true; // 如果是奇数个,跳过中间元素
}
}
return stack.size() == 0;
}
2.2 思路二:栈优化(快慢指针思想)
第一种方法有一个缺陷,就是我们必须自己去判断元素的个数以及链表元素个数的奇偶性,下面介绍基于快慢指针的优化方法。
快慢指针:设置两个指针,起始时两个指针都指向头结点,之后慢指针一次向前走一步,快指针一次向前走两步,当快指针来到链表末尾时,慢指针将指向链表的“中点”(注意:这里打上引号的原因是,在不同的题目要求下,我们可以略微调整指针的起始状态或者步进方式,使慢指针最终指向我们希望其指向的位置)
下面我们用快慢指针来对第一种方法进行优化:准备一个栈,假设此时快指针已经来到了链表的末尾而慢指针则指向了链表的中间节点,那么接下来我们只需要步进慢指针就可以将链表的后半部分都添加进栈中。待后半节点都已经加入了栈中,接下来我们只需要从链表头节点开始,与栈顶元素进行对比,如果元素相等则弹出栈顶元素,如果不等则证明该单链表不是回文串,最后如果栈为空则证明该链表是回文串。
public static Boolean isRoundList_Op(Node head) {
Stack<Node> stack = new Stack<>();
Node slow = head, quick = head;
while(quick.next!=null && quick.next.next!=null) {
// 这种条件判断方式保证慢节点最终来到链表的中间节点
// 中间节点指:奇数个元素链表的中间节点;偶数个元素俩表的中线前一个节点
quick = quick.next.next;
slow = slow.next;
}
quick = head;
while(slow.next != null) {
stack.push(slow.next);
slow = slow.next;
}
while(stack.size() != 0) {
if (!(quick.value == stack.pop().value)) {
return false;
}else {
quick = quick.next;
}
}
return true;
}
2.3 思路三:快慢指针 + 链表结构调整
有没有一种方法可以摆脱栈空间的使用呢?答案是肯定的!
如果我们有一种方法能够从链表的两头向中间前进,逐个对比那么我们将很轻易地判断一个链表是否为回文串。这就需要我们手动改变链表的结构,如下图:
public static Boolean isRoundList_Opp(Node head) {
Node slow = head, quick = head; // 快慢指针
while(quick.next!=null && quick.next.next!=null) {
slow = slow.next;
quick = quick.next.next;
} // 注意这里quick不一定指向最后一个节点
while(quick.next!=null) { // 是quick指针指向最后一个节点
quick =quick.next;
}
Node start = slow;
Node start_n = start.next;
Node start_nn = start_n.next;
while(start_nn!=null) {
start_n.next = start;
start = start_n;
start_n = start_nn;
start_nn = start_nn.next;
}
start_n.next = start;
slow.next = null;
start = head;
while(start!=null && quick!=null) {
if(start.value != quick.value) {
return false;
}
start = start.next;
quick = quick.next;
}
return true;
}
注意:链表的结构已经被我们改变了,如果需要保持原链表不变的话,还需要添加一段链表的结构恢复代码,如果感兴趣的话大家可以自己探索一下
未完待续,尽情期待!!
要是有收获的话,希望大家关注一波哦!!
3. 荷兰国旗问题(单链表版本)
题目说明:如上图所示,所谓的荷兰国旗问题就是依据数据的大小,将给定的数据集按一定的规则分成三个组别;特殊的,通常而言说起荷兰国旗问题指的是:给定一个num,将数据集分为“小于num组”,“等于num组”以及“大于num组”,对于本题我们要求小于num的组排列在前,等于num的排列在列表的中间,而大于num的元素则排在链表的最后。
3.1 思路一:基础Partition
3.2 思路二:优化Partition(基于链表本身的特性)
4. 特殊链表复制问题
题目说明:假设我们有一个特殊的链表结构,其中每个节点不仅仅有一个next指针(指向链表中的下一个对象),还有一个rand指针。其中rand指针可以指向该链表中的任意指针(也可以指向null)。而我们的要求则是,我们要完整地复制一份该链表(即复制每一个节点对象并完整地构建节点间连接关系)
4.1 思路一:借助其他数据结构
4.2 思路二:链表的结构调整与恢复
5. 判断链表是否相较返回第一个相交节点
题目说明:假设现在又两条链表(有可能有环,有可能无环),请判断这两条链表是否相交,如果相交,请返回这两条链表的第一个相交节点。