链表的基本性质:
链表和数组一样都是一种线性结构
①数组是一段连续的存储空间;
②链表空间不一定保证连续,为临时分配的。
链表的分类:
1.按连接方向分类:
①单链表
②双链表
2.按照有环无环分类
①普通链表
②循环链表:最后一个节点next指针指向第一个节点
链表问题代码实现的关键点:
1.链表调整函数的返回值类型,根据要求往往是节点类型;
2.处理链表过程中,先采用画图的方式理清逻辑;
3.链表问题对于边界条件讨论要求严格。
关于链表插入删除的注意事项:
1.特殊处理链表为空,或者链表长度为1的情况;
2.注意插入操作的调整过程:
同时找到插入位置之前的节点和之后的节点,然后把前一个节点的next的指针指向新插入的节点,最后把新插入节点的next指针指向后一个节点;
3.注意删除操作的调整过程:
找到删除节点的前一个节点,然后指向删除节点的后一个节点。
注意点:头尾节点及空节点需要特殊考虑,双链表的插入、删除和单链表类似,但是需要额外考虑previous指针的指向。
单链表的翻转操作:
1.当链表为空或者长度为1时,特殊处理;
2.对于一般情况:头节点head,当前结点now如:head->now->----*–>null
①将now结点的next指针指向head;
②将now设置为翻转部分的新头部;
③步骤①之前提前先用一个变量记录下now节点下一个节点是什么,然后继续步骤①步骤②。
大量链表问题可以使用额外数据结构来简化调整过程。
但链表问题最优解往往是不使用额外数据结构的方法。
例1:
给定一个整数num,如何在节点值有序的环形链表中插入一个节点值为num的节点,并且保证环形链表依然有序。
解:
将num定义为新节点node,如果链表为空,nodenext指向自己形成环形链表,返回node即可;
如果链表不为空,令变量p为头节点,变量c为第二个节点,然后p和c同步移动下去;如果此时p.val<=node.val并且c.val>=node.val,则将node插入到p和c之间,返回head即可;
如果p和c转了一圈都没有发现应该插入的位置,此时node应该插入到头节点的前面(最大或者最小)。如果是最小,应该返回node,node为新头部,这样才是有序的。
代码:
public ListNode insertNum(ListNode head, int num) {
ListNode node = new ListNode(num);
if (head == null) {
node.next = node;
return node;
}
ListNode pre = head;
ListNode cur = head.next;
while (cur != head) {
if (pre.val <= num && num <= cur.val) {
break;
}
pre = cur;
cur = cur.next;
}
pre.next = node;
node.next = cur;
return head.val < num ? head : node;
}
例2:
给定一个单链表的节点node,但不给定整个链表的头节点。如何在链表删除node?要求时间复杂度为O(1)。
解:
将node的值变为node下一个节点的值,然后删除node下一个节点。
代码:
public boolean removeNode(ListNode node) {
if (node == null) {
return false;
}
ListNode next = node.next;
if (next == null) {
return false;
}
node.val = next.val;
node.next = next.next;
return true;
}
例3:
给定一个链表的头节点head,再给定一个数num,请把链表调整成值小于num的节点都放在链表的左边,值等于num的节点放在链表的中间,值大于num的节点,放在链表的右边。
解:
简单做法:
1.将链表的所有节点放入到数组中,然后将数组进行快排划分的调整过程。
2.然后将数组中的节点重新串联。
最优解为:
将链表分为三个小链表,分别为值小于、大于、等于num的三个链表,然后重新连接起来。
代码:
public ListNode listDivide(ListNode head, int pivot) {
ListNode sh = null;// small head
ListNode st = null;
ListNode bh = null;// big head
ListNode bt = null;
ListNode next = null;
while (head != null) {
next = head.next;//存储 next node
head.next = null;
if (head.val <= pivot) {
if (sh == null) {
sh = head;
st = head;
} else {
st.next = head;
st = head;
}
} else {
if (bh == null) {
bh = head;
bt = head;
} else {
bt.next = head;
bt = head;
}
}
head = next;
}
if (st != null) {
st.next = bh;
}
return sh != null ? sh : bh;
}
例4:
给定两个有序链表的头节点head1和head2,打印两个链表的公共部分。
解:
如果两个链表有任何一种为空,直接返回即可;
否则,从两个链表的头节点开始比较,较小的节点往下移动,相等时,两个链表的节点都下移,当有任何一个链表为空,停止。
代码:
public int[] findCOmmonParts(ListNode head1, ListNode head2) {
LinkedList<Integer> list = new LinkedList<Integer>();
while (head1 != null && head2 != null) {
if (head1.val < head2.val) {
head1 = head1.next;
} else if (head1.val > head2.val) {
head2 = head2.next;
} else {
list.add(head1.val);
head1 = head1.next;
head2 = head2.next;
}
}
int[] res = new int[list.size()];
int index = 0;
while (!list.isEmpty()) {
res[index++] = list.pollFirst();
}
return res;
}
例5:
给定一个单链表的头节点head,实现一个调整单链表的函数,使得每K个节点之间逆序,如果最后不够K个节点一组,则不调整。
例如链表:
1->2->3->4->5->6->7->8
调整后为:
3->2->1->6->5->4->7->8
解:
方法一:时间复杂度为O(n),额外空间复杂度为O(k)。
利用栈,元素依次进栈,凑齐k个元素,依次出栈,第一组要特殊处理,需要返回的是节点3而不是几点1。
方法二:时间复杂度为O(n),额外空间复杂度为O(1)。
省去栈,依然是每收集k个元素就做逆序调整,记录下每一组的第一个节点,然后往下,当遍历到k个节点时逆序,然后将上一组调整好的尾结点与这一组的头节点相连。
代码:
public ListNode inverse(ListNode head, int K) {
if (K < 2) {
return head;
}
ListNode cur = head;
ListNode start = null;
ListNode pre = null;
ListNode next = null;
int count = 1;
while (cur != null) {
next = cur.next;
if (count == K) {
start = pre == null ? head : pre.next;
head = pre == null ? cur : head;
resign(pre, start, cur, next);
pre = start;
count = 0;
}
count++;
cur = next;
}
return head;
}
private void resign(ListNode left, ListNode start, ListNode end, ListNode right) {
// TODO 自动生成的方法存根
ListNode pre = start;
ListNode cur = start.next;
ListNode next = null;
while (cur != right) {
next = cur.next;
cur.next = pre;
pre = cur;
cur = next;
}
if (left != null) {
left.next = end;
}
start.next = right;
}
例6:
给定一个单链表的头节点head,再给定一个值val,把所有等于val的节点删除。
解:
将整个过程看成构造链表的过程,假设之前构造的链表头节点是head,尾节点是tail,如果当前节点now值为val就直接抛弃,否则就把该节点接到之前链表的末尾,此时需要改变末尾的next的指针,指向null节点,并且将null节点作为新的尾结点,特殊情况起初head和tail都为null,所以第一个接上的节点既是head也是tail,并且接入第一个的时候,tail是没有next指针的。
代码:
public ListNode clear(ListNode head, int num) {
while (head != null) {
if (head.val != num) {
break;
}
head = head.next;
}
ListNode pre = head;
ListNode cur = head;
while (cur != null) {
if (cur.val == num) {
pre.next = cur.next;
} else {
pre = cur;
}
cur = cur.next;
}
return head;
}
例7:
判断链表是否为回文结构
例如:
1->2->3->2->1,是回文结构,返回true,
1->2->3->1, 不是回文结构,返回false。
解:
方法一:时间复杂度O(n),使用n个额外空间;
将链表元素依次放入栈中,然后依次弹出,比对弹出元素是否与链表对应位置的元素值一样,如果每一步都相等说明是回文结构。
方法二:时间复杂度O(n),使用N/2个额外空间;
依然申请一个栈,定义两个指针(快慢指针),快指针一次走两步,慢指针一次走一步,慢指针遍历时将遍历过的节点压入栈,当快指针走完的时候,慢指针到了中间的位置,此时栈中的元素其实是链表左部分的逆序,如果链表长度为奇数,就不把中间节点压入栈中,接下来慢指针继续便利,栈开始弹出元素,对比栈弹出元素是否与遍历元素相等,如果每一步都相等则就是回文结构,否则就不是。
方法三:时间复杂度O(n),额外空间复杂度为O(1)。
前面过程与方法二一样找到中间节点,然后把链表右半边逆序调整,接下类从链表的两头开始依次对比节点值是否一样,如果到中间位置都一样,说明链表是回文结构。返回之前,将右半部分调整回来。
代码:
public boolean isPalindrome(ListNode head) {
if (head == null || head.next == null) {
return true;
}
ListNode n1 = head;
ListNode n2 = head;
while (n2.next != null && n2.next.next != null) {
n1 = n1.next;//n1->mid
n2 = n2.next.next;//n2->end
}
n2 = n1.next;//n2->右部分
n1.next = null;
ListNode n3 = null;
while (n2 != null) {
n3 = n2.next;
n2.next = n1;
n1 = n2;
n2 = n3;
}
n3 = n1;
n2 = head;
boolean res = true;
while (n1 != null && n2 != null) {
if (n1.val != n2.val) {
res = false;
break;
}
n1 = n1.next;
n2 = n2.next;
}
n1 = n3.next;
n3.next = null;
while (n1 != null) {
n2 = n1.next;
n1.next = n3;
n3 = n1;
n1 = n2;
}
return res;
}