链表
单链表反转
将链表的头结点变成尾结点
/**
* 翻转单链表
*
* @param head
* @return
*/
public static Node reverseLinkedList(Node head) {
// 预存前节点
Node pre = null;
// 预存后节点
Node next = null;
while (head != null) {
// 取出当前节点的后一个节点,保存下来
next = head.getNext();
// 翻转,将当前节点的next指向保存下来的前节点
head.setNext(pre);
// 将当前节点作为下一个节点的前节点,预保存下来
pre = head;
// 将当前接节点的指针向后移动,开启下一轮节点的翻转
head = next;
}
return pre;
}
双向链表反转
public static DoubleNode reverseDoubleList(DoubleNode head) {
// 预存前节点
DoubleNode pre = null;
// 预存后节点
DoubleNode next = null;
while (head != null) {
// 取出当前节点的后一个节点,保存下来
next = head.getNext();
// 翻转,将当前节点的next指向保存下来的前节点
head.setNext(pre);
// 翻转,将当前节点的pre指向保存下来的后节点
head.setPre(next);
// 将当前节点作为下一个节点的前节点,预保存下来
pre = head;
// 将当前接节点的指针向后移动,开启下一轮节点的翻转
head = next;
}
return pre;
}
单链表删除某一个值的节点
假设我们现有单链表 1 ->2->3->4->6->7…->null。如果我们需要删除value为某个值的节点,如需要删除value=1的节点,或者删除value=7的节点,我们该怎么做呢?
public static Node removeValueList(Node head, int num) {
// 如果链表的头结点以及后续节点,都是需要删除的节点,则head需要向后移动
while (head != null) {
if (head.getValue() != num) {
break;
}
head = head.getNext();
}
// 此时head来到了第一个不需要删除的位置
// 将当前节点的前节点保存下来,如果是head则为自身
Node pre = head;
// 从head开始遍历后续每一个节点,cur为当前被遍历的节点
Node cur = head;
while (cur != null) {
if (cur.getValue() == num) {
// 当前节点需要被删除,将当前节点的子节点,挂载到当前节点的父节点的next上,用于替代当前节点
pre.setNext(cur.getNext());
} else {
// 当前节点不需要被删除,前节点指针向后移动
pre = cur;
}
// 当前节点指针向后移动
cur = cur.getNext();
}
return head;
}
单链表,链表长度为基数则返回中点元素,偶数则返回上中点元素
这道题什么意思呢?
如现有链表 1 -> 2 -> 3 -> 4 -> 5,则需要返回元素3
如链表1 -> 2 -> 3 -> 4, 则中间元素为2,3,上中点元素为2。
分析:
目前我们只有链表的头结点head,根据需求,当我们链表遍历到中点/上中点位置时,我们必须要知道我们已经到达了中点位置。
我们可以使用快慢指针来完成这个功能,但是具体的边界值该如何确立呢?
慢指针移动一位,快指针需要移动两位。那为什么设置为移动两位呢?
我们模拟选快慢指针的移动过程:
0->1 代表从index为0的位置,移动到index=1的位置
慢指针0 -> 1,快指针0 ->2 , 快慢指针步长差值为2-1 =1 =慢指针的index
慢指针1 -> 2,快指针2->4, 快慢指针步长差值为4-2 =2 =慢指针的index
慢指针2 -> 3,快指针4->6, 快慢指针步长差值为6-3 =3 =慢指针的index
慢指针3 -> 4,快指针6->8, 快慢指针步长差值为8-4 =4 =慢指针的index
慢指针4 -> 5,快指针8->10,快慢指针步长差值为10-5 =5 =慢指针的index
假设我们现在慢指针正好在中点元素上,则链表的总长度为慢指针的index*2,换句话来讲,从链表的尾结点到我们的慢指针(此时为中间节点),这中间的长度正好等于慢指针的index。
我们通过设置指针的移动速度,正好可以让快慢指针之间正好间隔出半个链表的长度。当我们的慢指针到达中点元素时,我们快指针已经到达链表的末尾或者接近到达末尾。
由于题目中要求,如果链表长度是偶数,则返回的是上中点,也就是说在长度为基数情况下,快慢指针同步移动,如果是偶数的情况下,慢指针其实不动。这就要求快慢指针的起始位置并不是在一起的,而是一前一后,中间需要一个位差来调节技术偶数的情况。
当起始位置相差1之后,这两种情况就可以归并为一种场景考虑了。
如果链表长度为0,1,没有中点元素可以返回,如果长度为2,head即为上中点元素。这三种情况就不再考虑了。
我们假设现在链表只有3个元素的情况:
所以当慢指针在中点位置,或者上中点位置时,fast指针有两种可能,一种在tail位置,此时fast.next=null;还有一种可能是在tail的前一个节点位置,此时fast.next.next = null
public static Node midOrUpMidNode(Node head) {
// node -> node -> node(null)
// 如果链表长度为0,或者1,或者2,当前head即可满足题意
if (head == null || head.getNext() == null || head.getNext().getNext() == null) {
return head;
}
// 此时链表长度必然大于等于3
// 设置快慢指针,指定步长为1
Node slow = head.getNext();
Node fast = head.getNext().getNext();
while (fast.getNext() != null && fast.getNext().getNext() != null) {
// 此时slow还没有到达 中点/上中点 位置,可以继续向后移动
slow = slow.getNext();
fast = fast.getNext().getNext();
}
return slow;
}
单链表,链表长度为基数则返回中点元素,偶数则返回下中点元素
通过前面的分析,我们可以简单的分析出,slow和fast是需要同步移动的,中间不需要一个位差来调节奇数偶数的情况
public static Node midOrDownMidNode(Node head) {
// node -> node -> node(null)
// 如果链表长度为0,或者1,或者2,当前head即可满足题意
if (head == null || head.getNext() == null || head.getNext().getNext() == null) {
return head;
}
Node slow = head.getNext();
Node fast = head.getNext();
while (fast.getNext() != null && fast.getNext().getNext() != null) {
slow = slow.getNext();
fast = fast.getNext().getNext();
}
return slow;
}
单链表,链表是否是回文结构
什么是回文?像1221,12321这种左右对称的便是回文结构,那对应到链表又是什么情况呢?
由于单项链表只能从head遍历到tail,而判断是否回文,我们需要使用head元素与tail元素进行比较,两者的顺序正好相反。
我们可以借助栈先进后出的特性,正向遍历一次链表,然后将元素压入栈中。这样链表的head节点第一个入栈,成为栈里最里面一个元素,而链表的tail最后一个入栈,成为栈的栈顶元素。当进行出栈pop操作时,先出去的实际是tail节点数据,最后才是head节点数据。借助stack,我们可以实现类似从链表尾部向前遍历的功能。
public static boolean isPalindrome(Node head) {
Stack<Node> stack = new Stack<>();
Node cur = head;
// 将链表中所有元素入栈
while (cur != null) {
stack.push(cur);
cur = cur.getNext();
}
// 每一次正向遍历链表,从栈中pop出尾结点数据,如果数据不一致则不是回文
while (head != null) {
if (head.getValue() != stack.pop().getValue()) {
return false;
}
head = head.getNext();
}
return true;
}
是否还有另外的解法,不借助于stack呢?
我们来分析下这个功能,当我们遍历到中点元素的时候,此时如果我们能够从tail向head,也遍历到中点位置,那我们可以实现同样的功能。如何识别我们到了中点位置?不就是我们上面讲过的那个方法嘛。
当我们遍历到中点元素的时候,需要将后续的元素进行翻转,此时原链表会被分割成两部分:
public static boolean isPalindrome2(Node head) {
// 找到中点元素
Node midNode = midOrUpMidNode(head);
// 从中点元素的next开始,将后续链表进行翻转
Node reverseHead = reverseLinkedList(midNode.getNext());
// 现在已经将原有链表切割成了两个链表了,将两个链表依次进行比较
Node originCurrent = head;
Node reverseCurrent = reverseHead;
boolean isPalindrome = true;
while (originCurrent != null && reverseCurrent != null) {
// 判断正向和逆向的元素是不是一样的
if (originCurrent.getValue() != reverseCurrent.getValue()) {
isPalindrome = false;
break;
}
originCurrent = originCurrent.getNext();
reverseCurrent = reverseCurrent.getNext();
}
// 将两个链表重新恢复到原始链表
midNode.setNext(reverseLinkedList(reverseHead));
return isPalindrome;
}
这个可能不是很好理解,我们结合实际运行结果来看下:
我们原始链表中有9个元素,id分别为1,2,3,4,5,6,7,8,9,value为1,2,3,4,5,4,3,2,1,原始链表如下:
当我们找到中点元素后,对后续的链表进行翻转,原始链表就被切割成了2份:
此时示意图如下:
此时我们要做的就是从正向的head向右遍历链表,逆向的head向左遍历链表,查看移动相同位数的情况下,数值是否相等。
最后再将拆分后的两个链表还原成原始的链表。
单链表,如何按照某一个值进行分区
如现有链表 1->4->5->3->6->3,以3位界限,区分 小于3 的区域 ,等于3区域,大于3的区域。
按照需求,需要将上面的原始链表最终转换为下面啊的链表。
方案一:
可以借助三个外部数组,先遍历一次链表,分别将数据放入小于3的数组里,等于3 的数组里,大于3的数组里。最后分别遍历3个数组,组装出新的链表返回即可
方案二:
我们首先定义出6个引用变量,类型为链表的Node类型,分别代表 小于区的head和tail,等于区的head和tail,大于区的head和tail。
然后让指针current从head开始进行遍历:
-
current=1时,1属于小于区,将small head和small tail都指向1,current指针移向next,同时1断开next:
-
current到达4,归属于大于区,步骤同上:
-
current到达5,归属于大于区域,由于big head 和big tail都是引用类型,通过这两个引用,我们就可以找到节点4,现在我们需要节点4的next指向5,同时设置5为big tail:
-
current=3,归属等于区,同步骤 1 / 2:
-
current=6,归属于大于区,步骤同3:
-
current=3,归属于等于区,方法同上:
-
根据引用关系,将small tail 的next设置为 equal head ,将equal tail 的next设置为 big head,这样便构建出一个新的链表:
这样我们就可以得到新的链表 1->3->3->4->5->6
单链表,如何判断其中有环
我们来分析下单链表有环的情况下有几种可能性:
- head和tail直接形成环
- 中间某一个环节形成环
那我们如何判断一个链表中是否有环呢?
方案一:
借助hashSet,我们知道HashSet中数据是不能重复的,如果我们将链表中的每一个元素都放入HashSet中,一旦出现add失败,则链表中一定存在环结构,且add失败的节点即为环交接的节点。
方案二:
可以使用快慢指针,慢指针每次走一步,快指针每次走两步,如果链表存在环,则快慢指针一定会在换上的某一个节点相遇。
为演示,随机取一个点做为相遇点:
那我们如何确定环的交接点是哪个呢?从slow和fast指针的相遇处开始,fast指针回到head节点,slow还停留在原地,如何两个指针都以步长1开始移动,最终两个指针的相遇点即为环的交汇点:
/**
* 找到链表的第一个入环节点,如果链表无环则返回null
*
* @param head
* @return
*/
public static Node getLoopNode(Node head) {
if (head == null || head.getNext() == null || head.getNext().getNext() == null) {
return null;
}
Node slow = head.getNext();
Node fast = head.getNext().getNext();
// 找到快慢指针相遇的点
while (slow != fast) {
if (fast.getNext() == null || fast.getNext().getNext() == null) {
return null;
}
fast = fast.getNext().getNext();
slow = slow.getNext();
}
// fast回到head,开始依次往下走,寻找环的交接点
fast = head;
while (slow != fast) {
fast = fast.getNext();
slow = slow.getNext();
}
return slow;
}
判断两个单链表是否相交
链表无环
两个链表不想交:
相交的情况:
两个链表一旦相交,从相交节点开始,必然只会有一条线路,因为节点的next指针只有一个,所以肯定不会有分叉的情况出现。
所以我们怎么判断两个链表会相交呢?
方案一:
和判断是否有环一样,两个链表相交,那必然是会公用同一个节点的。我们还是可以借助hashSet:
遍历链表1,将链表1中的所有node节点放入hashSet中
再遍历链表2,将链表2的node节点也放入hashSet中。如果出现add失败,则add失败的节点,即为交汇点。
方案二:
根据我们前面的分析,如果两个链表有相交,则其尾结点一定会重合的。如果交汇点就是尾结点,则尾结点重合。如果交汇点事尾结点的前一个节点,则尾结点还是会重合的。
按照这个结论,我们往前进行推理:
假设现在两个链表,一个长一个短。长度分别为5和7。则从尾结点开始往前的5个长度之内,两个链表必有交汇点。换而言之,短链表从head开始遍历,长链表从 第(7-5)+1个节点开始,和以相同的步长开始遍历,在5个长度之内,必然会找到交汇点。
/**
* 如果两个无环链表相交,返回第一个交汇节点
*
* @param head1
* @param head2
* @return
*/
public static Node noLoop(Node head1, Node head2) {
if (head1 == null || head2 == null) {
return null;
}
Node current1 = head1;
Node current2 = head2;
// 记录两个链表的长度之差
int lengthDiff = 0;
// 遍历第一个链表,获取其长度
while (current1.getNext() != null) {
lengthDiff++;
current1 = current1.getNext();
}
// 遍历第二个链表,获取其与第一个链表的长度的差值
while (current2.getNext() != null) {
lengthDiff--;
current2 = current2.getNext();
}
// 此时两个链表都到达了尾结点,如果两个尾结点还是没有交汇,说明两个链表不会相交
if (current1 != current2) {
return null;
}
// 强制current1指向长度较长的那个链表
current1 = lengthDiff > 0 ? head1 : head2;
// 强制current2指向长度较短的那个链表
current2 = current1 == head1 ? head2 : head1;
// 挪动长链表的指针,直至长链表剩余长度与短链表长度相同
lengthDiff = Math.abs(lengthDiff);
while (lengthDiff != 0) {
lengthDiff--;
current1 = current1.getNext();
}
// 长度相同之后,两个链表同步向后移动,两者最终会同步到达交汇点,也即current1 == current2
while (current1 != current2) {
current1 = current1.getNext();
current2 = current2.getNext();
}
return current1;
}
链表有环
总结下来会有这三种场景。
我们可以通过上面的getLoopNode(head),找到每一个链表的入环节点,假设链表1的入环节点为loop1,链表2的入环节点是loop2。
分析一下情况1和情况2,我们可以看到两个链表的入环节点都是A,也就是链表1的loop1=A,链表2的loop2=A,此时loop1=loop2。此时的节点A必然是两个链表的重合点。我们可以不再关系节点A后续的所有节点了,只需要关心链表1从head到节点A,链表2从head到节点A,这这区间链表上,哪一个节点是交汇点。这种场景下,可以使用上面”无环链表的交汇点”的方案进行解决。这种场景我们只要判断loop1 == loop2:
我们要如何判断是否是场景3呢?
链表1的入环节点是A,而链表2的入环节点是B。这两个节点是不可能相等的。由于这两个点都位于环上。所以从A节点出发,通过next必然可以到达B节点,同理,B节点通过next必然也可以到达A节点。所以这种情况下,只要从loop1可以遍历到loop2,或者从loop2可以遍历到loop1,那么loop1和loop2都是交汇点。