1. 怎么判断链表是否有环
2. LRU算法
3. 数组中第K大个元素
4. K个一组翻转链表
5. 三数之和
6. 最大子数组和
一 怎么判断链表是否有环
快慢指针法:定义快慢指针,快指针每次走两步,慢指针每次走一步,如果无环,那么快指针会提前到底终点,否则,快慢指针会相遇。有一个非常简单的证明方法,但慢指针进入环时,假设快慢指针移动方向上距离K,那么另一端方向距离就是N-K(N为环长度),那么在移动方向上K是按+1变大的因为快指针比慢指针快一步,那么N-K就是按-1变小的,那么N-K终究会变为0,即快慢指针终会相遇。
实现代码(java):
public boolean hasCycle(ListNode head) {
if (head == null || head.next == null) {
return false;
}
ListNode slow = head;
ListNode fast = head.next;
while (slow != fast) { // slow=fast就相遇了,立即跳出
// 在首次判断后,快指针始终比慢指针快,所以只要判断快指针是否为空即可,
// 另外因为快指针是跳两步的,那么避免报错,next也要遍历的
if (fast == null || fast.next == null) { // 到这里说明快指针到终点了,无环
return false;
}
slow = slow.next;
fast = fast.next.next;
}
return true;
}
实现代码(Python):
def hasCycle(head):
if not head or not head.next: // None也一样
return false
slow, fast = head, head.next
while slow != fast:
if not fast or not fast.next:
return False
slow = slow.next
fast = fast.next.next
return True
实现代码(C++):
bool hasCycle(ListNode *head) {
if (head == nullptr || head->next == nullptr) { // C++11新特性,用nullptr较为好
return false;
}
ListNode *slow = head;
ListNode *fast = head->next;
while (slow != fast) {
if (fast == nullptr || fast->next == nullptr) {
return false;
}
slow = slow->next;
fast = fast->next->next;
}
return true;
}
实现代码(Go):
func hasCycle(head *ListNode) bool {
if head == nil || head.Next == nil {
return false
}
slow, fast := head, head.Next
for slow != fast {
if fast == nil || fast.Next == nil {
return false
}
slow = slow.Next
fast = fast.Next.Next
}
return true
}
二 LRU算法
一种缓存淘汰算法,核心思想:当缓存空间满时,优先淘汰掉最近最少使用的缓存数据。
描述:维护一个缓存数据的访问时间列表,每次访问缓存数据时,将该数据的访问时间更新到列表的末尾;当缓存空间满时,淘汰掉访问时间最早的缓存数据。
实现:
-
定义一个双向链表和一个哈希表,前者维护缓存数据的访问顺序,后者快速查找缓存数据。
-
当有访问缓存数据时, 在hash表中查找是否存在,若存在,则解链表,把该节点删除原位置后放首位,并返回值,若不存在,则hash表中添加并在链表首项添加(hash的k-v即缓存key-链表节点)。
-
空间满删除时,把链表尾数据依次删除,并据链表存的hash的key去删除hash表中的数据。
代码实现(java):
class LRUCache {
class ListNode {
int key;
int value;
ListNode pivot;
ListNode next;
ListNode() {}
ListNode(int key, int value) {this.key=key; this.value=value;}
}
int capacity = 0;
HashMap<Integer, ListNode> map;
ListNode head = new ListNode();
ListNode tail = new ListNode();
public LRUCache(int capacity) {
this.capacity = capacity;
map = new HashMap<>();
head.next = tail;
tail.pivot = head;
}
public int get(int key) {
if (map.containsKey(key)) {
ListNode node = map.get(key);
removeHead(node);
return node.value;
} else {
return -1;
}
}
public void put(int key, int value) {
if (map.containsKey(key)) {
ListNode node = map.get(key);
node.value = value;
removeHead(node);
} else {
if (capacity == map.size()) {
map.remove(deleteTail());
}
ListNode node = new ListNode(key, value);
map.put(key, node);
makehead(node);
}
}
private void removeHead(ListNode node) {
deleteNode(node);
makehead(node);
}
private int deleteTail() {
ListNode node = tail.pivot;
deleteNode(node);
return node.key;
}
private void deleteNode(ListNode node) {
node.pivot.next = node.next;
node.next.pivot = node.pivot;
}
private void makehead(ListNode node) {
node.next = head.next;
node.pivot = head;
head.next.pivot = node;
head.next = node;
}
}
代码实现(Python):
class LinkNode:
def __init__(self, key=0, value=0):
self.key = key
self.value = value
self.prev = None
self.next = None
class LRUCache(object):
def __init__(self, capacity):
self.map = dict() # 创建hash map
self.head = LinkNode()
self.tail = LinkNode()
self.head.next = self.tail
self.tail.prev = self.head
self.capacity = capacity
def get(self, key):
if key in self.map:
node = self.map[key]
self.removeHead(node)
return node.value
else:
return -1
def put(self, key, value):
if key in self.map:
node = self.map[key]
node.value = value
self.removeHead(node)
else:
if self.capacity == len(self.map):
self.map.pop(self.deleteTail())
node = LinkNode(key, value)
self.map[key] = node
self.makehead(node)
def removeHead(self, node):
self.deleteNode(node)
self.makehead(node)
def deleteTail(self):
node = self.tail.prev
self.deleteNode(node)
return node.key
def deleteNode(self, node):
node.prev.next = node.next
node.next.prev = node.prev
def makehead(self, node):
node.prev = self.head
node.next = self.head.next
self.head.next.prev = node
self.head.next = node
代码实现(C++):
class LRUCache {
private:
struct ListNode {
int key;
int value;
ListNode* pivot;
ListNode* next;
ListNode() : key(0), value(0), pivot(nullptr), next(nullptr) {}
ListNode(int k, int v) : key(k), value(v), pivot(nullptr), next(nullptr) {}
};
int capacity;
unordered_map<int, ListNode*> map;
ListNode* head;
ListNode* tail;
public:
LRUCache(int capacity) {
this->capacity = capacity;
head = new ListNode();
tail = new ListNode();
head->next = tail;
tail->pivot = head;
}
int get(int key) {
if (map.count(key) == 0) {
return -1;
}
ListNode* node = map[key];
removeHead(node);
return node->value;
}
void put(int key, int value) {
if (map.count(key) > 0) {
ListNode* node = map[key];
node->value = value;
removeHead(node);
} else {
if (map.size() == capacity) {
map.erase(deleteTail());
}
ListNode* node = new ListNode(key, value);
map[key] = node;
makehead(node);
}
}
private:
void deleteNode(ListNode* node) {
node->pivot->next = node->next;
node->next->pivot = node->pivot;
}
void makehead(ListNode* node) {
node->next = head->next;
node->pivot = head;
head->next->pivot = node;
head->next = node;
}
void removeHead(ListNode* node) {
deleteNode(node);
makehead(node);
}
int deleteTail() {
ListNode* node = tail->pivot;
deleteNode(node);
int key = node->key;
delete node; // C++操作防止内存泄漏
return key;
}
};
代码实现(Go):
type LRUCache struct {
size int
capacity int
cache map[int]*LinkNode
head, tail *LinkNode
}
type LinkNode struct {
key, value int
prev, next *LinkNode
}
func initLinkNode(key, value int) *LinkNode {
return &LinkNode{
key: key,
value: value,
}
}
func Constructor(capacity int) LRUCache {
l := LRUCache{
cache: map[int]*LinkNode{},
head: initLinkNode(0, 0),
tail: initLinkNode(0, 0),
capacity: capacity,
}
l.head.next = l.tail
l.tail.prev = l.head
return l
}
func (this *LRUCache) Get(key int) int {
if _, ok := this.cache[key]; ok {
node := this.cache[key]
this.removeHead(node)
return node.value
} else {
return -1
}
}
func (this *LRUCache) Put(key int, value int) {
if node, ok := this.cache[key]; ok {
node.value = value
this.removeHead(node)
} else {
if len(this.cache) == this.capacity {
delete(this.cache, this.deleteTail())
}
node := initLinkNode(key, value)
this.cache[key] = node
this.makehead(node)
}
}
func (this *LRUCache) makehead(node *LinkNode) {
node.prev = this.head
node.next = this.head.next
this.head.next.prev = node
this.head.next = node
}
func (this *LRUCache) removeNode(node *LinkNode) {
node.prev.next = node.next
node.next.prev = node.prev
}
func (this *LRUCache) removeHead(node *LinkNode) {
this.removeNode(node)
this.makehead(node)
}
func (this *LRUCache) deleteTail() int {
node := this.tail.prev
this.removeNode(node)
return node.key
}
三 数组中的第K个最大元素
来源于一经典问题:线性时间选择。
描述:基于快速排序思想对分开区域不做排序且中位索引随机方式查找,一首先分区,随机生成中位索引,在范围[l, r]中,把索引值X记录并与r位置交换,遍历l->r-1,i从l开始,将比x小的值都记录到i中,最后再将i的最后索引位与r交换,返回i值,得到以i索引为中心的两边大小分区;二递归查找,根据分区大小判断是否找到第K大,否则按长度进入对应分区寻找第K大。
代码实现(java):
class Solution {
public int findKthLargest(int[] nums, int k) {
return quickSelect(nums, 0, nums.length - 1, nums.length - k);
}
private int quickSelect(int[] nums, int l, int r, int k) {
int q = select(nums, l, r);
if (q == k) {
return nums[q];
} else {
return k < q ? quickSelect(nums, l, q - 1, k) : quickSelect(nums, q + 1, r, k);
}
}
private int select(int[] nums, int l, int r) {
int randomIndex = (new Random()).nextInt(r - l + 1) + l;
swag(nums, randomIndex, r); // 现在r的位置放的是random
int mid = l - 1;
for (int i = l; i < r; i++) {
if (nums[i] < nums[r]) {
swag(nums, i, ++mid);
}
}
swag(nums, ++mid, r);
return mid;
}
private void swag(int[] nums, int i, int j) {
int temp = nums[i];
nums[i] = nums[j];
nums[j] = temp;
}
}
四 K 个一组翻转链表
想法1:因为不能直接读到链表长度,那么计数即可,读到链表节点数到k,则反转,直接节点为空暂停遍历一个节点A记录链表每次的头,另一个节点B记录链表尾,然后最后节点B的下一位进入循环,用栈做辅助,后进先出,巧好移动A达到反转k个的目的(思路可以,但最终的速度和空间都不太行)
优化 ——> 链表断开反转,再接上,不过每次反转,都需要在反转的链表头加一个辅助节点,且每次走k个点时头和尾要同起点,另外由于原头节点也会被反转,所以还需要一个全局额外的头节点,以返回结果。
代码实现(java):
class Solution {
public ListNode reverseKGroup(ListNode head, int k) {
ListNode root = new ListNode(0, head); // 0 -> (head ->)
ListNode end = root;
head = root; // 从头节点开始
while (end != null) {
for (int i = 0; i < k && end != null; i++) {
end = end.next;
}
if (end == null) {
break;
}
ListNode head_ = head.next;
ListNode end_ = end.next; // 记录不需反转的后续位置
end.next = null; // 这样传入反转链表才不会破坏后面的
head.next = reverse(head_);
head_.next = end_; // 此时的head_就相当于原链表中的end位置
// head和end往下一个循环走,同起点,且不触及下一个会使用的节点
head = head_;
end = head_;
}
return root.next;
}
// 反转链表,cur-root-next一气呵成,cur、root是操作核心,next是记录后者
public ListNode reverse(ListNode root) {
ListNode cur = null;
while (root != null) {
ListNode next = root.next;
root.next = cur;
cur = root;
root = next;
}
return cur;
}
}
五 三数之和
核心:排序 + 双指针法
注意一个重要的限制条件,即三元组不重复。
描述:双指针需要先排序,设i: 0->len-3遍历,由后面两个指针搜索达到sum=0值数,显然排序后,i必然是最下的那个,即nums[i]必然<=0,故可以把nums[i]>0作为一移动条件。当遍历i后,若nums[i]与nums[i-1]相同,那么跳过,因为nums[i-1]会遍历出对应的情况【三元组不重复】。然后l=i+1 - r=len-1开始搜寻,只要l<r,那么在sum>0时左移,sum<0时右移。
代码实现(java):
class Solution {
public List<List<Integer>> threeSum(int[] nums) {
List<List<Integer>> res = new ArrayList<>();
Arrays.sort(nums);
for (int i = 0; i < nums.length - 2; i++) {
if (nums[i] > 0) return res;
if (i > 0 && nums[i] == nums[i - 1]) continue;
int l = i + 1;
int r = nums.length - 1;
while (l < r) {
int sum = nums[i] + nums[l] + nums[r];
if (sum < 0) {
l++;
} else if (sum > 0) {
r--;
} else {
List<Integer> list = new ArrayList<>();
list.add(nums[i]);
list.add(nums[l]);
list.add(nums[r]);
res.add(list);
while (l < r && nums[l] == nums[l + 1]) l++; // 排掉使用过的相同元素,i固定,那么只要nums[l]不变,nums[r]值必然不变
while (l < r && nums[r] == nums[r - 1]) r--; // 排掉使用过的相同元素
l++; // l+1位还是一样的,需要再往后一位
r--;
}
}
}
return res;
}
}
六 最大子数组和
动态规划(优):
初始化:f[0] = nums[0]
状态转移方程:f[i] = max(f[i - 1] + nums[i] , nums[i]) , i>0
res = max(res, f[i])
对于任意的nums[i],显然只有前面累计值为正时,才会更大,往前想也如此,状态方程即这样出来的。
代码实现(java):
public int maxSubArray(int[] nums) {
// dp[i]代表使用nums[i]的连续最大和
int[] dp = new int[nums.length];
// 初始化
dp[0] = nums[0];
int res = dp[0];
for (int i = 1; i < nums.length; i++) {
dp[i] = Math.max(dp[i-1] + nums[i], nums[i]);
res = Math.max(res, dp[i]);
}
return res;
}
分治法:
在不考虑单元素取值的情况下,中间向两边扩散,分治到底选出最大值,分治到最后也会拿单元素比较,但扩缩时以至少两边元素和来做扩散。
public class Solution {
public int maxSubArray(int[] nums) {
return maxSubArraySum(nums, 0, nums.length - 1);
}
// 计算至少包含 nums[mid]和nums[mid+1] 的最大和(这里是按左右两边累计找和)
private int maxCrossingSum(int[] nums, int left, int mid, int right) {
int sum = 0;
int leftSum = Integer.MIN_VALUE, rightSum = Integer.MIN_VALUE;
// 左半边包含 nums[mid] 元素,最多可以到什么地方,即计算以 mid 结尾的最大的子数组的和
for (int i = mid; i >= left; i--) {
sum += nums[i];
if (sum > leftSum) {
leftSum = sum;
}
}
sum = 0;
// 右半边不包含 nums[mid] 元素,最多可以到什么地方,即计算以 mid+1 开始的最大的子数组的和
for (int i = mid + 1; i <= right; i++) {
sum += nums[i];
if (sum > rightSum) {
rightSum = sum;
}
}
return leftSum + rightSum;
}
private int maxSubArraySum(int[] nums, int left, int right) {
if (left == right) {
return nums[left]; // 考虑一个元素的情况最终会落到这里,而不是maxCrossingSum
}
int mid = left + (right - left) / 2;
return max3(maxSubArraySum(nums, left, mid),
maxSubArraySum(nums, mid + 1, right),
maxCrossingSum(nums, left, mid, right)); // 一直分治拆到每一个包含的最大和做比较
}
private int max3(int num1, int num2, int num3) {
return Math.max(num1, Math.max(num2, num3));
}
}
// 例如数组为4大小的数组
// su(0,1), su(2,3) c(0,1,3)
// su(0,0), su(1,1) c(0,0,1)
// su(2,2), su(3,3) c(2,2,3)