链表简介
链表分为单链表、双链表:
单链表:链表中的每个元素实际上是一个单独的对象,而所有的对象通过每个元素中的引用字段(next)字段链接在一起;
双链表:与单链表不同的是,双链表中每个元素通过两个引用字段链接在一起;
链表的出现,在某种程度上是为了避免数组的一大缺陷:即分配数组的时候需要分配一段连续的内存空间;但鱼和熊掌不可兼得,链表也牺牲了数组的一些优点:链表不能通过下标快速查询元素。因此在考虑是否需要用链表解决算法问题时,务必想清楚该问题是否需要频繁的查询和遍历。
链表优缺点
优点
- 灵活地分配内存空间;
- 添加、删除元素快,时间复杂度O(1);(前提:单链表中该元素的前一个元素已知;双链表中该元素的前一个元素或后一个元素已知【后续技巧中创建临时表头的意义所在】)
缺点
- 不能像数组那样通过下标迅速访问元素,每次查询需要从链表的头开始向后遍历,直到查询到对应元素;时间复杂度O(n);
是否采用链表权衡:
- 如果待解问题中需要频繁快速查询,则链表并不适合(可考虑数组);
- 待解问题中,数据元素个数不确定,经常需要进行数据的添加、删除,则链表更合适;
解题技巧
- 利用快慢指针(有时候需要用到3个指针)
- 链表的反转;
- 寻找倒数第K个元素;
- 寻找链表中中间位置的元素;
- 判断链表是否有环 ;
- 构建一个临时链表头(一般应用在要返回一个新的链表头的题目中)
- 给定两个有序链表,进行整合排序;
- 将链表的奇数偶数分开后,返回前半部分为奇数、后半部分为偶数的新的链表;
训练方法
在纸上或白板上画出链表元素之间的指引关系,手动完成增删改等操作,这样有助于理解抽象过程,在面试过程中清楚地表达自己的解题思路;(亲测高效)
经典算法题解
LeetCode24
方法一:递归
代码:
/**
* 递归
* @param head
* @return
*/
public static ListNode swapPairs(ListNode head){
if(null == head || null == head.next) return head;
ListNode next = head.next;
head.next = swapPairs(head.next.next);
next.next = head;
return next;
}
方法二:非递归
思路:
- 创建临时链表头newHead,使其next指向head;
- 创建pre指针,初始指向newHead;
- 交换pre后地两个指针pre.next(node1)和pre.next.next(node2);
- pre指针指向node1;
- 重复3,4;
- 边界条件:pre后面不足两个节点;
代码:
/**
* 非递归
* @param head
* @return
*/
public static ListNode swapPairs2(ListNode head){
ListNode newHead = new ListNode(-1);
newHead.next = head;
ListNode pre = newHead;
while(null != pre.next && null != pre.next.next){
ListNode n1 = pre.next;
ListNode n2 = pre.next.next;
ListNode next = n2.next;
n1.next = next;
n2.next = n1;
pre.next = n2;
pre = n1;
}
return newHead.next;
}
LeetCode25
思路
- 创建临时链表表头newHead,newHead.next=head;
- 创建左右指针l、r,初始指向newHead;
- 创建计数器cnt = 0;
- r一步步向右移动遍历链表,同时计cnt++计数。
遍历过程中若cnt==k:调用Helper(l,r);【目的:反转l.next至r之间的链表,并返回该子链表反转后的末尾节点】
l、r指针指向该末尾节点;
计数器清零; - 返回newHead.next;(因为反转后head已经不是表头,这也是创建临时表头的目的之一)
【Helper(l,r)方法】
目的:反转l.next至r之间的链表,并返回该子链表反转后的末尾节点。
例如对于链表-1->1->2->3,左指针指向-1,右指针指向3,Helper函数执行后,1至3链表被反转,并返回-1->3->2->1的末尾节点1.
递归思路:
反转[l.next,r]子链表,即将l.next与( [l.next.next,r]反转后的子链表)交换。
临界条件:l.next== r,即待反转节点只有一个,直接返回r;
细节解惑:为什么Helper方法参数中,左指针指向待反转链表表头的前一个节点,而非表头?
反例解析:假设对于链表-1->1->2->3,我们需要反转子链表1->2。
此时如果我们传递的左右指针:左指针指向1,右指针指向2。
反转操作:ListNode rNext = 2.next; //3
2.next = 1;
1.next = rNext; //3
此时就出现问题,1->2反转后的子链表2->1,无法与子链表表头的的前一个节点-1链接,
即-1.next = 2无法完成,因为参数指针没有指向子链表表头前一个节点的指针,导致断链;
代码
/**
* 将链表head每K个一组进行反转
* @param head
* @param k
* @return
*/
public ListNode reverseKGroup(ListNode head, int k) {
ListNode newHead = new ListNode(-1);
newHead.next = head;
ListNode l = newHead,r = newHead;
int cnt = 0;
while(r != null){
if(cnt == k){
r = reverHelper(l,r);
l = r;
cnt = 0;
}
cnt++;
r = r.next;
}
return newHead.next;
}
/**
* 将[l.next,r]反转,并返回反转后地链表rail
* @param l
* @param r
* @return
*/
public ListNode reverHelper(ListNode l,ListNode r){
if(l.next == r){
return r;
}
r = reverHelper(l.next,r);
//ListNode temp1 = l.next;
ListNode temp2 = r.next;
r.next = l.next;
l.next = l.next.next;
r.next.next = temp2;
return r.next;
}