文章目录
数组和链表
提及数组和链表。习惯性地就想起了数组的查找是O(1)、平均插入和删除的时间复杂度为O(N)。链表查找是O(N)、插入和删除是O(1)。
仔细想想,这样的说法并不完全正确。这里的查找是说根据下标来查找,而不是查找根据元素值来进行查找。比如有个数组,a[ ],访问数组的第三个元素(下标从0开始),直接用a[2]就可以得到第三个元素,而链表的话并不支持这样的快速访问,你要得到第三个元素,必须一个一个遍历,需要一直数到第三个元素。原因在于,数组的地址是连续的,我们知道第一个元素的地址:base。知道数组中元素的大小:size,比如一个int占4个字节。然后就可以通过这么一个计算公式得到第k个元素的地址:base + (k - 1)size 然后就可以进行访问。而链表的存储空间并不连续,所以无法这样计算。
也就是说:根据下标来查找,数组的时间复杂度为O(1),链表为O(N)。
如果不是根据下标来查找,而是根据值来查找呢?也就是说我们不是要找第三个元素,而是要找有没有3这个元素。那不好意思,数组和链表的时间复杂度都是O(N),因为必须得从头开始遍历,一个一个找。如果数组中的元素是有序的,那么可以用二分查找,时间复杂度可以降低为O(logn),但是达不到O(1)。如果链表中的元素也是有序的呢,可以用跳表这种数据结构,用空间换时间,也可以将查找的时间复杂度降低为O(logn)。
那有没有一种数据结构,根据元素值来进行查找可以达到O(1)的时间复杂度呢,目前我所了解的算法里面只有Hash表能达到这样的效果,而且还得看Hash函数是否设计得好。
本篇设计到的题目
链表题目练习(包含代码)
ListNode节点的定义以及相应的操作方法
为了方便在本机调试,我还写了一些方法用于生成链表
,返回链表中的第K个节点
,返回链表的遍历结果
。为了简单,我也没使用泛型,假设链表中存储的都是int类型的数据。
import java.util.ArrayList;
import java.util.List;
/**
* 链表的定义
* @author simon zhao
*/
public class ListNode {
int val;
ListNode next;
public ListNode(int val) {
this.val = val;
}
/**
* 根据数组生成LinkedList
*
* @param nums
* @return
*/
public static ListNode generateLinkedList(int[] nums) {
if (nums == null || nums.length == 0) {
return null;
}
ListNode head = new ListNode(nums[0]);
ListNode cur = head;
for (int i = 1; i < nums.length; ++i) {
ListNode node = new ListNode(nums[i]);
cur.next = node;
cur = node;
}
return head;
}
/**
* 遍历LinkedList,返回一个List<Integer>
*
* @param head
* @return
*/
public static List<Integer> traverseLinkedList(ListNode head) {
if (head == null) {
return null;
}
List<Integer> traverse = new ArrayList<>();
ListNode cur = head;
while (cur != null) {
traverse.add(cur.val);
cur = cur.next;
}
return traverse;
}
/**
* 返回LinkedList中index这个ListNode
*
* @param head
* @param index
* @return
*/
public static ListNode getIndexNode(ListNode head, int index) {
if (head == null) {
return null;
}
int count = 0;
ListNode cur = head;
while (cur != null) {
if (count == index) {
return cur;
}
if (count < index) {
count++;
cur = cur.next;
}
}
//如果LinkedList都已经遍历完毕,但是count依然小于index,说明index过大,超出了LinkedList的长度
return null;
}
}
LeetCode206. 反转链表
题目链接:LeetCode206. 反转链表
题目概要
反转一个单链表。
示例:
输入: 1->2->3->4->5->NULL
输出: 5->4->3->2->1->NULL
一段精简的Python代码
def reverseList(self, head):
cur, prev = head, None
while cur:
cur.next, prev, cur = prev, cur, cur.next
return prev
需要注意的是:cur.next, prev, cur = prev, cur, cur.next
并不是先进行cur.next = prev
,然后进行prev = cur
。而是多步赋值操作一起进行的,堪称一步到位。
用Java代码实现
方法一:使用三个指针进行迭代
public ListNode reverseList(ListNode head) {
if (head == null) {
return head;
}
ListNode cur = head, pre = null, next;
while (cur != null) {
next = cur.next;
cur.next = pre;
pre = cur;
cur = next;
}
return pre;
}
方法二:使用递归
public ListNode reverseList(ListNode head) {
if (head == null || head.next == null) {
return head;
}
ListNode p = reverseList(head.next);
head.next.next = head;
head.next = null;
return p;
}
递归代码不好理解,可以看图
LeetCode141. 环形链表
题目链接:LeetCode141. 环形链表
题目概要
给定一个链表,判断链表中是否有环。
如果链表中有某个节点,可以通过连续跟踪 next 指针再次到达,则链表中存在环。 为了表示给定链表中的环,我们使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。 如果 pos 是 -1,则在该链表中没有环。注意:pos 不作为参数进行传递,仅仅是为了标识链表的实际情况。
如果链表中存在环,则返回 true 。 否则,返回 false 。
方法一:使用快慢指针
采用快慢指针,快指针每次走两步,慢指针每次走一步,如果快慢指针能相遇,则说明有环。
public boolean hasCycle(ListNode head) {
if (head == null || head.next == null) {
return false;
}
ListNode quick = head.next.next, slow = head;
while (quick != slow) {
if (quick == null || quick.next == null) {
return false;
}
quick = quick.next.next;
slow = slow.next;
}
return true;
}
虽然上面这段代码能通过LeetCode,但是逻辑有问题,应该将slow = head
修改为slow = head.next
。不然slow
指针就起跑慢了。
方法二:暴力法
暴力法。循环Integer.MAX_VALUE / 30次,若能发现null,则说明无环,在给定次数内,都没发现null,则认为是有环的,,该方法不一定准确,但是能通过LeetCode。
public boolean hasCycle(ListNode head) {
ListNode cur = head;
for (int i = 0; i < Integer.MAX_VALUE / 30; ++i) {
if (cur == null || cur.next == null) {
return false;
}
cur = cur.next;
}
return true;
}
方法三:使用Set存储遍历过的节点
使用Set将遍历过的节点保存下来,每次走到新的节点时先查询Set中是否存在该节点,若存在,则说明有环
public boolean hasCycle(ListNode head) {
Set<ListNode> visited = new HashSet<>();
ListNode cur = head;
while (true) {
if (cur == null) {
return false;
}
if (visited.contains(cur)) {
return true;
}
visited.add(cur);
cur = cur.next;
}
}
LeetCode24.两两交换链表中的节点
题目概要
给定一个链表,两两交换其中相邻的节点,并返回交换后的链表。
你不能只是单纯的改变节点内部的值,而是需要实际的进行节点交换。
方法一:使用递归
想到了反转链表中的递归解决,这个解法就很容易想到了
需要注意的是,当节点个数为奇数时,最后一个是不用交换的。节点个数为偶数时,两两相邻节点会被交换。
public ListNode swapPairs1(ListNode head) {
if (head.next == null || head == null) {
return head;
}
ListNode next = swapPairs1(head.next.next);
ListNode pre = head.next;
head.next = next;
pre.next = head;
return pre;
}
如果对这段代码不是很理解,可以结合下图来辅助理解,整个过程是从右到左的。
方法二:使用指针进行迭代(从左至右)
使用递归的解法是从右到左的顺序来进行交换。使用指针从左到右来进行交换。
public ListNode swapPairs(ListNode head) {
if (head == null || head.next == null) {
return head;
}
ListNode cur = head;
head = head.next;
//这是一个工具节点,用来指示当前节点的上一个节点
ListNode last = new ListNode(-1);
while (cur != null && cur.next != null) {
ListNode next = cur.next.next;
ListNode prev = cur.next;
prev.next = cur;
cur.next = next;
last.next = prev;
last = cur;
cur = next;
}
return head;
}
对上面的代码不理解,可以参照下图便于理解
LeetCode142. 环形链表 II
题目链接:LeetCode142. 环形链表 II
题目概要
给定一个链表,返回链表开始入环的第一个节点。 如果链表无环,则返回 null。
为了表示给定链表中的环,我们使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。 如果 pos 是 -1,则在该链表中没有环。注意,pos 仅仅是用于标识环的情况,并不会作为参数传递到函数中。
说明:不允许修改给定的链表。
这个题目和LeetCode141. 环形链表看起来差不多。区别在于一个仅需要判断有环没环,另一个则需要返回入环的第一个节点。
刚做这个题目时,第一时间想到了快慢指针,因此将LeetCode141. 环形链表的快慢指针的代码修改了下便进行提交,发现根本通过不了,这才想到了问题所在:快慢指针相遇的节点并不一定是入环的第一个节点
。
方法一:使用Set存储遍历过的节点
public ListNode detectCycle(ListNode head) {
if (head == null || head.next == null) {
return null;
}
Set<ListNode> visited = new HashSet<>();
ListNode cur = head;
while (cur != null) {
if (visited.contains(cur)) {
return cur;
}
visited.add(cur);
cur = cur.next;
}
return null;
}
方法二:使用快慢指针
快慢指针,可以在环的任一位置相遇。因此不能将快指针 = 慢指针
的这个节点作为入环的第一个节点返回。
那么入环的第一个节点与快慢指针相遇的节点之间是否有关系呢?当然有!
任意时刻,快指针走过的距离都为慢指针的 2 倍。因此,我们有:
a+(n+1)b+nc=2(a+b)⟹a=c+(n−1)(b+c)
。n代表的是快指针绕环的圈数。
当快指针和慢指针相遇时,我们再额外使用一个指针ptr
,它指向链表头部;随后,它和 慢指针每次向前移动一个位置。最终,它们会在入环点相遇。为什么呢?
看这个等式:a=c+(n−1)(b+c)
。当ptr
指针从head遍历到入环的第一个节点
时,ptr
走过的距离为a。
那慢指针呢?根据等式,慢指针走过的距离为c + (n−1)(b+c)
。n代表的是快指针绕环的圈数,肯定会大于等于1,而且一定是个正整数。b+c
刚好就是整个环的距离(也就是环的周长)。慢指针走过了c这么一段距离以后来到了入环的第一个节点
,还需要走(n−1)(b+c)
这么长的距离,相当于是b还要绕n - 1圈。不管是绕几圈,b最终都会回到入环的第一个节点
。而此时ptr
也走过了a这么长的距离来到了入环的第一个节点
,两个指针便相遇了。
详情可以查看官方题解:LeetCode官方题解
第一次写的代码,直接根据LeetCode141. 环形链表中的快慢指针改了改。
对比这两段代码,我仅仅新增了蓝色方框的这部分代码。
看起来好像没什么错误,一提交就发现过不了,显示超时。为什么呢?问题出在红色方框的这部分。
当quick = head.next.nex
t的时候,slow应该为head.next
。即快指针走两步时,慢指针应该走一步。但是图中的代码却是slow = head
。明显slow起跑晚了。
这会导致什么问题嗯?左边这段代码是LeetCode141. 环形链表中的,这代码能在LeetCode中通过,尽管这是一段错误的代码。
为什么呢?
在仅仅需要判断是否有环的时候,尽管slow指针的起步慢了。但是slow指针终究还是会进入环。一旦进入环,由于quick的移动速度是slow的两倍,相当于是在环形跑道上进行追逐,不管slow在这个环形跑道的任何位置,由于quick的速度是slow的两倍,quick一定会追上slow的。可以看一下下面这个图便于理解:
但是,如果继续保持这样的代码,在本题就不适用了。
看下图。如果初始是slow = head
,那么最终slow
和quick
会相遇在2这个节点。这个时候,ptr
和slow
是永远也不会相遇的,那就更别提在2这个节点相遇。
如果初始是slow = head.next
,那么最终slow
和quick
会相遇在-4这个节点,这个时候ptr
和slow
会在2这个节点相遇。
最终通过的代码如下:
public boolean hasCycle(ListNode head) {
if (head == null || head.next == null) {
return false;
}
ListNode quick = head.next.next, slow = head.next;
while (quick != slow) {
if (quick == null || quick.next == null) {
return false;
}
quick = quick.next.next;
slow = slow.next;
}
return true;
}
LeetCode25. K 个一组翻转链表
题目链接:LeetCode25. K 个一组翻转链表
题目概要
给你一个链表,每 k 个节点一组进行翻转,请你返回翻转后的链表。
k 是一个正整数,它的值小于或等于链表的长度。
如果节点总数不是 k 的整数倍,那么请将最后剩余的节点保持原有顺序。
示例:
给你这个链表:1->2->3->4->5
当 k = 2 时,应当返回: 2->1->4->3->5
当 k = 3 时,应当返回: 3->2->1->4->5
方法一:使用递归
前面的链表题目用来很多递归的解法,写着写着手就顺了。
这个题目就是LeetCode24. 两两交换链表中的节点的困难版。在两两交换链表中的节点中我们只用考虑两个节点之间的交换,而在这里面我们要考虑K个节点的交换,因此还要结合一下LeetCode206. 反转链表 中的内容。
我们要将这K个节点看作一个整体,每次移动K个节点进行交换。根据递归,首先移动都最后的节点,如果最后的节点数不满K,就直接返回这一串链表,不需要对这些链表翻转。
还有一种情况,就是当K=1的时候,其实也不需要进行翻转,保持原样即可。
在递归的程序中,我是先判断从当前的head节点开始(包括head节点)是否有K个节点,如果节点数目根本不够K个,则不需要对其做任何操作,直接返回head。返回的这个节点被作为上一组节点的next节点。
如果从当前的head结点开始计数,发现链表中节点的数目满足K个,则反转从head节点开始(包括head节点)的K个节点。反转的代码其实和LeetCode206. 反转链表中的递归代码类似。
/**
* k个一组翻转链表,如果k==1,则不发生翻转
* <p>
* 给定链表:1->2->3->4->5
* 当 k = 2 时,应当返回: 2->1->4->3->5
* 当 k = 3 时,应当返回: 3->2->1->4->5
*
* @param head
* @param k
* @return
*/
public ListNode reverseKGroup(ListNode head, int k) {
//k = 1,时,不发生翻转,head == null说明是走到了末尾或者是head为null
if (head == null || k == 1) {
return head;
}
ListNode next;
ListNode kthNodeFromHead = getKthNodeFromHead(head, k);
if (kthNodeFromHead != null) {
next = reverseKGroup(kthNodeFromHead.next, k);
} else {
return head;
}
//对当前的k个元素进行翻转,翻转以后,最后一个元素变为第一个元素,第一个元素变为最后一个元素
ListNode last = reverseKNode(head, kthNodeFromHead);
last.next = next;
return kthNodeFromHead;
}
/**
* 获得从当前的head节点开始的第K个节点,比如head -> 1 -> 2 -> 3 -> 4获得k = 3的节点,
* 即返回值为3的这个节点
*
* @param head
* @param k
* @return 如果能得到第K个节点,则返回该节点,否则返回Null
*/
public ListNode getKthNodeFromHead(ListNode head, int k) {
int count = 1;
ListNode cur = head;
while (cur != null) {
cur = cur.next;
count++;
if (count == k) {
return cur;
}
}
return null;
}
/**
* 返回翻转以后的链表的最后一个节点,比如翻转[head -> 1 -> 2 -> 3 ]
* 翻转以后变为[3 -> 2 -> 1],返回1这个节点
* @param head
* @param end
* @return
*/
public ListNode reverseKNode(ListNode head, ListNode end) {
if (head == end) {
return head;
}
ListNode prev = reverseKNode(head.next, end);
prev.next = head;
return head;
}
方法二:使用迭代
复用了方法一的一些代码。
public ListNode reverseKGroup(ListNode head, int k) {
if (head == null || k == 1 || getKthNodeFromCur(head, k) == null) {
return head;
}
ListNode prev = new ListNode(-1), cur = head;
head = getKthNodeFromCur(head, k);
while (getKthNodeFromCur(cur, k) != null) {
ListNode kthNodeFromCur = getKthNodeFromCur(cur, k);
//存储kthNodeFromCur的下一个节点
ListNode next = kthNodeFromCur.next;
//翻转以后kthNodeFromCur变为这个区间内链表的头节点,reverseKNode返回这个区间的最后一个节点
ListNode last = reverseKNode(cur, kthNodeFromCur);
prev.next = kthNodeFromCur;
prev = last;
cur = next;
}
prev.next = cur;
return head;
}
public ListNode getKthNodeFromCur(ListNode head, int k) {
int count = 1;
ListNode cur = head;
while (cur != null) {
cur = cur.next;
count++;
if (count == k) {
return cur;
}
}
return null;
}
public ListNode reverseKNode(ListNode head, ListNode end) {
if (head == end) {
return head;
}
ListNode prev = reverseKNode(head.next, end);
prev.next = head;
return head;
}