-
重排链表
-
最长递增子序列
-
环形链表
-
反转链表
-
最长回文子串
-
全排列
-
lru 缓存
-
合并k个升序链表
-
无重复字符的最长子串
-
删除链表的倒数第 n 个结点
1. 重排链表
给定一个单链表 l 的头节点 head ,单链表 l 表示为:
l0 → l1 → … → ln - 1 → ln
请将其重新排列后变为:
l0 → ln → l1 → ln - 1 → l2 → ln - 2 → …
输入:
head = [1,2,3,4]
输出:
[1,4,2,3]
思路:
如果是数组就好了,哈哈,因为数组可以直接通过下标访问,很容易就可以解答这道题了。但是这是链表。**链表不支持下标访问,**我们没办法随机访问到链表任意位置的元素,怎么办呢?
我们可以先遍历一下,用数组把链表的元素按顺序存储起来呀,然后就可以把它当做数组这么访问来用了对吧,最后重建下链表即可啦。
arraylist的底层就是数组,我们先用它存储链表就好,如下:
list list = new arraylist();
listnode node = head;
while (node != null) {
list.add(node);
node = node.next;
}
有了一个数组结构的链表后,如何重建链表呢?回头多看示例两眼,很容易就发小规律啦:先排第1个,再排倒数第1个,接着排第2个,紧接着倒数第2个。显然这个规律很明显,代码也比较好实现:
int i = 0;
int j = list.size()-1;
while(i
完整实现代码如下:
class solution {
public void reorderlist(listnode head) {
if (head == null) {
return;
}
list list = new arraylist();
listnode node = head;
while (node != null) {
list.add(node);
node = node.next;
}
int i = 0, j = list.size() - 1;
while (i < j) {
list.get(i).next = list.get(j);
i ;
if (i == j) {
break;
}
list.get(j).next = list.get(i);
j--;
}
list.get(i).next = null;
}
}
2. 最长递增子序列
给你一个整数数组 nums ,找到其中最长严格递增子序列的长度。
实例1:
输入:nums = [10,9,2,5,3,7,101,18]
输出:4
解释:最长递增子序列是 [2,3,7,101],因此长度为 4 。
实例2:
输入:nums = [0,1,0,3,2,3]
输出:4
思路:
这道题是求最值问题,可以使用动态规划解决。动态规划的解题整体思路就是:
-
穷举分析
-
分析找规律,拆分子问题
-
确定边界
-
确定最优子结构
-
写出状态转移方程
2.1 穷举分析
动态规划的核心思想包括拆分子问题,记住过往,减少重复计算。所以我们在思考原问题:数组num[i]的最长递增子序列长度时,可以思考下相关子问题,比如原问题是否跟子问题num[i-1]的最长递增子序列长度有关呢?
自底向上的穷举过程:
-
当nums只有一个元素10时,最长递增子序列是[10],长度是1.
-
当nums需要加入一个元素9时,最长递增子序列是[10]或者[9],长度是1。
-
当nums再加入一个元素2时,最长递增子序列是[10]或者[9]或者[2],长度是1。
-
当nums再加入一个元素5时,最长递增子序列是[2,5],长度是2。
-
当nums再加入一个元素3时,最长递增子序列是[2,5]或者[2,3],长度是2。
-
当nums再加入一个元素7时,,最长递增子序列是[2,5,7]或者[2,3,7],长度是3。
-
当nums再加入一个元素101时,最长递增子序列是[2,5,7,101]或者[2,3,7,101],长度是4。
-
当nums再加入一个元素18时,最长递增子序列是[2,5,7,101]或者[2,3,7,101]或者[2,5,7,18]或者[2,3,7,18],长度是4。
-
当nums再加入一个元素7时,最长递增子序列是[2,5,7,101]或者[2,3,7,101]或者[2,5,7,18]或者[2,3,7,18],长度是4.
2.2 分析找规律,拆分子问题
通过上面分析,我们可以发现一个规律:
如果新加入一个元素nums[i]
, 最长递增子序列要么是以nums[i]
结尾的递增子序列,要么就是nums[i-1]
的最长递增子序列。看到这个,是不是很开心,nums[i]
的最长递增子序列已经跟子问题 nums[i-1]
的最长递增子序列有关联了。
原问题数组nums[i]的最长递增子序列 = 子问题数组nums[i-1]的最长递增子序列/nums[i]结尾的最长递增子序列
是不是感觉成功了一半呢?但是如何把nums[i]
结尾的递增子序列也转化为对应的子问题呢?要是nums[i]
结尾的递增子序列也跟nums[i-1]
的最长递增子序列有关就好了。又或者nums[i]
结尾的最长递增子序列,跟前面子问题num[j](0=结尾的最长递增子序列有关就好了,带着这个想法,我们又回头看看穷举的过程:
nums[i]
的最长递增子序列,不就是从以数组num[i]
每个元素结尾的最长子序列集合,取元素最多(也就是长度最长)那个嘛,所以原问题,我们转化成求出以数组nums每个元素结尾的最长子序列集合,再取最大值嘛。哈哈,想到这,我们就可以用dp[i]
表示以num[i]
这个数结尾的最长递增子序列的长度啦,然后再来看看其中的规律:
其实,nums[i]
结尾的自增子序列,只要找到比nums[i]
小的子序列,加上nums[i]
就可以啦。显然,可能形成多种新的子序列,我们选最长那个,就是dp[i]
的值啦
-
nums[3]=5,以5结尾的最长子序列就是[2,5],因为从数组下标0到3遍历,只找到了子序列[2]比5小,所以就是[2] [5]啦,即dp[4]=2
-
nums[4]=3,以3结尾的最长子序列就是[2,3],因为从数组下标0到4遍历,只找到了子序列[2]比3小,所以就是[2] [3]啦,即dp[4]=2
-
nums[5]=7,以7结尾的最长子序列就是[2,5,7]和[2,3,7],因为从数组下标0到5遍历,找到2,5和3都比7小,所以就有[2,7],[5,7],[3,7],[2,5,7]和[2,3,7]这些子序列,最长子序列就是[2,5,7]和[2,3,7],它俩不就是以5结尾和3结尾的最长递增子序列 [7]来的嘛!所以,dp[5]=3 =dp[3] 1=dp[4] 1。
很显然有这个规律:一个以nums[i]结尾的数组nums
- 如果存在j属于区间[0,i-1],并且num[i]>num[j]的话,则有:dp(i) =max(dp(j)) 1。
2.3 确定边界
当nums数组只有一个元素时,最长递增子序列的长度dp(1)=1,当nums数组有两个元素时,dp(2) =2或者1, 因此边界就是dp(1)=1。
2.4 确定最优子结构
从2.2 穷举分析找规律,我们可以得出,以下的最优结构:
dp(i) =max(dp(j)) 1,存在j属于区间[0,i-1],并且num[i]>num[j]。
max(dp(j)) 就是最优子结构。
2.5 写出状态转移方程
通过前面分析,我们就可以得出状态转移方程啦:
所以数组nums[i]的最长递增子序列就是:
最长递增子序列 =max(dp[i])
完整代码实现如下:
class solution {
public int lengthoflis(int[] nums) {
if (nums.length == 0) {
return 0;
}
int[] dp = new int[nums.length];
//初始化就是边界情况
dp[0] = 1;
int maxans = 1;
//自底向上遍历
for (int i = 1; i < nums.length; i ) {
dp[i] = 1;
//从下标0到i遍历
for (int j = 0; j < i; j ) {
//找到前面比nums[i]小的数nums[j],即有dp[i]= dp[j] 1
if (nums[j] < nums[i]) {
//因为会有多个小于nums[i]的数,也就是会存在多种组合了嘛,我们就取最大放到dp[i]
dp[i] = math.max(dp[i], dp[j] 1);
}
}
//求出dp[i]后,dp最大那个就是nums的最长递增子序列啦
maxans = math.max(maxans, dp[i]);
}
return maxans;
}
}
3. 环形链表
给定一个链表的头节点head ,返回链表开始入环的第一个节点。如果链表无环,则返回 null。
实例:
输入:head = [3,2,0,-4], pos = 1
输出:返回索引为 1 的链表节点
解释:链表中有一个环,其尾部连接到第二个节点。
如果判断链表是否有环,我们可以使用快慢指针,快指针是慢指针速度的两倍,当两个指针相遇时,即表示有环。
boolean hascycle(listnode head ){
listnode slow = head;
listnode fast = head;
while(fast!=null && fast.next!=null){
fast = fast.next.next;
slow = slow.next;
if(fast==slow){
return true;
}
}
return false;
}
我们可以很容易就判断有环,但是如何返回入环的第一个节点呢?我们来画个图分析一波:
假设起点为a,入环点为b,快慢指针相遇点为c,慢指针走到相遇点为k
步,b到c的距离为m
。设环型周长为x。因为快指针速度是慢指针的2倍。则有:
k-m x m = 2k = 快指针走的举例
所以周长x = k
。相遇后,快指针到继续往前走,走到入环点b,刚好距离是x-m = k-m
。而起点到b节点,距离也是k-m
。因此,快慢指针相遇后,慢指针回到起点,这时候快慢指针一样的速度走,相遇时,就是入环点啦,是不是无巧不成书呀,哈哈哈。
完整代码如下:
public class solution {
public listnode detectcycle(listnode head) {
if(head ==null){
return null;
}
listnode fast = head;
listnode slow = head;
while(fast!=null&&fast.next!=null){
fast = fast.next.next;
slow = slow.next;
//快慢指针相等表示有环
if(slow==fast){
//回到起点一起相同速度走
while(head!=fast){
head = head.next;
fast = fast.next;
}
return head;
}
}
return null;
}
}
4. 反转链表
给你单链表的头节点 head ,请你反转链表,并返回反转后的链表。
输入:head = [1,2,3,4,5]
输出:[5,4,3,2,1]
完整代码如下:
class solution {
public listnode reverselist(listnode head) {
listnode prev = null;
listnode next = head;
listnode curr = head ;
while(curr!=null){
next = curr.next ;
curr. next = prev;
prev = curr ;
curr = next ;
}
return prev;
}
}
之前图解过这道题,大家可以看下哈:
看一遍就理解,图解单链表反转
5. 最长回文子串
给你一个字符串 s,找到 s 中最长的回文子串。
实例1:
输入:s = "babad"
输出:"bab"
解释:"aba" 同样是符合题意的答案。
这道题可以使用中心扩展法实现,从中间开始向两边扩散来判断回文串。
for 0 <= i < len(s):
找到以 s[i] 为中心的回文串
更新答案
但是回文串可能是长度可能是奇数,也可能是偶数,因此需要加多一步:
for 0 <= i < len(s):
找到以 s[i] 为中心的回文串
找到以 s[i] 和s[i 1] 为中心的回文串
更新答案
完整代码如下:
class solution {
public string longestpalindrome(string s) {
if(s==null|| s.length()<2){
return s;
}
string result ="";
for(int i=0;ir2.length()? r1 :r2;
result = tempmax.length()> result.length()?tempmax:result;
}
return result;
}
private string sublongestpalindrome(string s,int l,int r){
while(l>=0&&r
6.全排列
给定一个不含重复数字的数组 nums ,返回其 所有可能的全排列 。你可以 按任意顺序 返回答案。
示例 1:
输入:nums = [1,2,3]
输出:[[1,2,3],[1,3,2],[2,1,3],[2,3,1],[3,1,2],[3,2,1]]
示例 2:
输入:nums = [0,1]
输出:[[0,1],[1,0]]
这道题可以用回溯算法解决,完整代码如下:
class solution {
//全排列,即所有路径集合
list> allpath = new linkedlist<>();
public list> permute(int[] nums) {
//当前路径,入口路径,path是空的
list path = new linkedlist<>();
//递归函数入口,可做选择是nums数组
backtrace(nums,path);
return allpath;
}
public void backtrace(int[] nums,list path){
//已走路径path的数组长度等于nums的长度,表示走到叶子节点,所以加到全排列集合
if(nums.length==path.size()){
allpath.add(new linkedlist(path));
return;
}
for(int i=0;i
大家可以看下这篇回溯文章哈,有回溯算法的框架套用。
面试必备:回溯算法详解
7. lru 缓存
请你设计并实现一个满足 lru (最近最少使用) 缓存 约束的数据结构。
实现 lrucache 类:
-
lrucache(int capacity) 以 正整数 作为容量 capacity 初始化 lru 缓存
-
int get(int key) 如果关键字 key 存在于缓存中,则返回关键字的值,否则返回 -1 。
-
void put(int key, int value) 如果关键字 key 已经存在,则变更其数据值 value ;如果不存在,则向缓存中插入该组 key-value 。如果插入操作导致关键字数量超过 capacity ,则应该 逐出 最久未使用的关键字。
函数 get 和 put 必须以o(1)
的平均时间复杂度运行。
示例:
输入
["lrucache", "put", "put", "get", "put", "get", "put", "get", "get", "get"]
[[2], [1, 1], [2, 2], [1], [3, 3], [2], [4, 4], [1], [3], [4]]
输出
[null, null, null, 1, null, -1, null, -1, 3, 4]
解释
lrucache lrucache = new lrucache(2);
lrucache.put(1, 1); // 缓存是 {1=1}
lrucache.put(2, 2); // 缓存是 {1=1, 2=2}
lrucache.get(1); // 返回 1
lrucache.put(3, 3); // 该操作会使得关键字 2 作废,缓存是 {1=1, 3=3}
lrucache.get(2); // 返回 -1 (未找到)
lrucache.put(4, 4); // 该操作会使得关键字 1 作废,缓存是 {4=4, 3=3}
lrucache.get(1); // 返回 -1 (未找到)
lrucache.get(3); // 返回 3
lrucache.get(4); // 返回 4
这道题,出现的频率还是挺高的,很多小伙伴在面试时,都反馈自己遇到过原题。
lru,least recently used,即最近使用过的数据是有用的,可以使用双链表 hashmap
解答,双链表用于存储lrucache的数据,hashmap实现o(1)的平均时间复杂度。
-
每次从链表尾部添加元素,靠尾的元素就是最近使用过
-
某个key可以通过哈希表快速定位到节点。
对于双链表,需要做哪些事呢。
-
首先是链表初始化,为了方便处理i,虚拟一个头节点和尾结点。
-
添加元素时,放到链表的尾部,表示该元素最近使用过
-
删除双向链表的某个节点
-
删除并返回头节点,表示删除最久未使用的元素
-
返回链表当前长度
lru缓存有哪些方法
-
构造函数初始化方法
-
get和put方法
-
makerecently 设置某个元素最近使用过的方法,哈希表已经有该元素
-
addrecently 添加最近使用过的元素,同时更新map
-
deletekey 删除某个key对应的元素,同时删除map上的节点
-
removeleastrecently 删除最久未使用的元素
完整代码如下:
class node {
int key,val;
node next,prev;
public node(int key,int val){
this.key = key;
this.val = val;
}
}
class doublelist {
//虚拟出头节点和尾结点
private node head, tail;
private int size;
//初始化双链表
public doublelist() {
//虚拟头结点
head = new node(0, 0);
//虚拟头结点
tail = new node(0, 0);
head.next = tail;
tail.prev = head;
size = 0;
}
//要加到链表尾部,且越靠近链表尾部,越表示最近使用过
public void addlast(node x) {
//比如当前链表为:head <-> 1 <-> tail,加入结点x = 2
x.prev = tail.prev;
// 完成结点2指向两端的箭头 head <-> 1 <- 2 -> tail; 此时tail.pre = 结点1还未断开
x.next = tail;
//head <-> 1 <-> 2 -> tail;
tail.prev.next = x;
//head <-> 1 <-> 2 <-> tail;
tail.prev = x;
//更新链表长度
size ;
}
// 删除指定结点
public void remove(node x) {
x.prev.next = x.next;
x.next.prev = x.prev;
size--;
}
// 删除并返回头结点
public node removehead() {
if (head.next == tail) {
return null;
}
node first = head.next;
// size在remove中更新了
remove(first);
// 用作在哈希表中移除最久未使用的数据值
return first;
}
// 获取链表长度
public int getsize() {
return size;
}
}
public class lrucache {
private map map;
private doublelist doublelist;
private int cap;
public lrucache(int capacity) {
this.map = new hashmap<>();
this.doublelist = new doublelist();
this.cap = capacity;
}
public int get(int key) {
if (map.containskey(key)) {
// 先将key标记为最近使用,再返回value
makerecently(key);
return map.get(key).val;
} else {
return -1;
}
}
public void put(int key, int value) {
if (map.containskey(key)) {
deletekey(key); // 从原map中移除该key
addrecently(key, value); // 更新最近使用
return;
}
int size = doublelist.getsize();
if (size == cap) {
// 说明需要移除最久未使用的元素了
removeleastrecently();
}
addrecently(key, value); //添加新的元素进来
}
public void makerecently(int key) {
// 将某个key标记为最近使用的元素(map中已存在的)
node x = map.get(key);
doublelist.remove(x); // 先从双链表删除
doublelist.addlast(x); // 再添加到链表末尾, 因为尾部是最近使用过的元素
}
public void addrecently(int key, int value) {
// 添加最近使用过的元素
node x = new node(key, value);
doublelist.addlast(x);
map.put(key, x); //更新map
}
public void deletekey(int key) {
node x = map.get(key);
map.remove(key);
doublelist.remove(x); // 在map中和cache中同时删除
}
// 删除最久未使用的元素
public void removeleastrecently() {
// 最久未使用的一定在链表头部
node oldnode = doublelist.removehead();
int oldkey = oldnode.key;
map.remove(oldkey);
}
}
8. 合并k个升序链表
给你一个链表数组,每个链表都已经按升序排列。请你将所有链表合并到一个升序链表中,返回合并后的链表。
示例 1:
输入:lists = [[1,4,5],[1,3,4],[2,6]]
输出:[1,1,2,3,4,4,5,6]
解释:链表数组如下:
[
1->4->5,
1->3->4,
2->6
]
将它们合并到一个有序链表中得到。
1->1->2->3->4->4->5->6
合并两个有序链表,是比较简单的,相信大家都会做。那么如何合并k个有序链表呢?其实道理是一样的,我们可以借用优先级队列找出最小节点,完整代码如下:
class solution {
public listnode mergeklists(listnode[] lists) {
if(lists.length==0){
return null;
}
//虚拟节点
listnode head = new listnode(0);
listnode tail = head;
//优先队列
priorityqueue queue = new priorityqueue<>(lists.length,(a, b)->(a.val-b.val));
//将k个链表头节点合并最小堆
for (listnode node: lists) {
if (node != null) {
queue.add(node);
}
}
while (!queue.isempty()) {
//获取最小节点,放到结果链表中
listnode node = queue.poll();
tail.next = node;
if (node.next != null) {
queue.add(node.next);
}
//指针链表一直往前
tail = tail.next;
}
return head.next;
}
}
9. 无重复字符的最长子串
给定一个字符串 s ,请你找出其中不含有重复字符的 最长子串 的长度。
示例 1:
输入: s = "abcabcbb"
输出: 3
解释: 因为无重复字符的最长子串是 "abc",所以其长度为 3。
示例 2:
输入: s = "bbbbb"
输出: 1
解释: 因为无重复字符的最长子串是 "b",所以其长度为 1。
这道题可以使用滑动窗口来实现。滑动窗口就是维护一个窗口,不断滑动,然后更新答案。
滑动窗口的大致逻辑框架,伪代码如下:
int left =0,right = 0;
while (right < s.size()){
//增大窗口
window.add(s[right]);
right ;
while (window needs shrink){
//缩小窗口
window.remove (s[left]);
left ;
}
}
解法流程如下:
-
首先呢,就是获取原字符串的长度。
-
接着维护一个窗口(数组、哈希、队列)
-
窗口一步一步向右扩展
-
窗口在向右扩展滑动过程,需要判断左边是否需要缩减
-
最后比较更新答案
完整代码如下:
int lengthoflongestsubstring(string s){
//获取原字符串的长度
int len = s.length();
//维护一个哈希集合的窗口
set windows = new hashset<>();
int left=0,right =0;
int res =0;
while(right
之前写过一篇滑动窗口解析,大家有兴趣可以看下哈:
leetcode必备算法:聊聊滑动窗口
10.删除链表的倒数第 n 个结点
给你一个链表,删除链表的倒数第 n 个结点,并且返回链表的头结点。
示例 :
输入:head = [1,2,3,4,5], n = 2
输出:[1,2,3,5]
这道题可以使用双指针解决。既然我们要找到倒数第n
个节点,我们可以使用两个指针first 和 second
同时对链表进行遍历,并且first 比second
超前 n
个节点。当 first
遍历到链表的末尾时,second
就恰好处于倒数第n
个节点。
class solution {
public listnode removenthfromend(listnode head, int n) {
listnode dummy = new listnode(0, head);
listnode first = head;
listnode second = dummy;
//first 比second先走n个节点
for (int i = 0; i < n; i) {
first = first.next;
}
//直到走到链表尾部
while (first != null) {
first = first.next;
second = second.next;
}
//删除节点
second.next = second.next.next;
listnode ans = dummy.next;
return ans;
}
}