一、挑战字符串
1.无重复字符的最长子串
给定一个字符串,请你找出其中不含有重复字符的 最长子串 的长度。
输入: “abcabcbb”
输出: 3
解释: 因为无重复字符的最长子串是 “abc”,所以其长度为 3。
示例 2:
输入: "bbbbb"a
输出: 1
解释: 因为无重复字符的最长子串是 “b”,所以其长度为 1。
示例 3:
输入: “pwwkew”
输出: 3
解释: 因为无重复字符的最长子串是 “wke”,所以其长度为 3。
请注意,你的答案必须是 子串 的长度,“pwke” 是一个子序列,不是子串。
import java.util.HashSet;
import java.util.Set;
//滑动窗口法;初始化滑动窗口,长度为1,窗口内容为s.charAt(0);若窗口右侧字母不存在与窗口内,
// 说明窗口可以拓展,则向右拓展;否则不可以拓展,记录下当前窗口大小,并且将窗口向右缩小,
// 即将窗口左侧index加一
public class _1无重复的最长子串 {
public static int lengthOfLongestSubstring(String s) {
int length =s.length();
int max=0;
int i=0;
int j=0;
Set<Character> set = new HashSet<>();
while (i<length&&j<length){
if (!set.contains(s.charAt(j))){
set.add(s.charAt(j));
j++;
max=Math.max(max,j-i);
}else {
set.remove(s.charAt(i));
i++;
}
}
return max;
}
public static void main(String[] args) {
String s ="pwwkew";
System.out.println(lengthOfLongestSubstring(s));
}
}
2.最长公共前缀
编写一个函数来查找字符串数组中的最长公共前缀。
如果不存在公共前缀,返回空字符串 “”。
示例 1:
输入: [“flower”,“flow”,“flight”]
输出: “fl”
示例 2:
输入: [“dog”,“racecar”,“car”]
输出: “”
解释: 输入不存在公共前缀。
//解法2:垂直扫描法(见下代码)
//
// 另外一种方法是,我们不像刚刚的方法一样将最长公共子串由长变短,而是由短变长。我们从0开始迭代,
// 对于每次迭代,我们验证index=i的位置上的所有字符串是否都为同一字符,以此类推,从而得到最长公共子串
public class _2最长公共前缀 {
public static String longestCommonPrefix(String[] strs) {
if (strs.length==0){
return "";
}
Boolean flag=true;
int current=0;
StringBuilder stringBuilder=new StringBuilder();
while (flag){
Character character=null;
for (String s:strs){
if (current<s.length()){
if (character==null){
character=s.charAt(current);
}else {
if (!character.equals(s.charAt(current))){
flag=false;
break;
}
}
}else {
flag=false;
}
}
if (flag&&character!=null){
stringBuilder.append(character);
}
current++;
}
return stringBuilder.toString();
}
public static void main(String[] args) {
String[] s ={"dog","racecar","car"};
System.out.println(longestCommonPrefix(s));
}
}
3.字符串的排列
给定两个字符串 s1 和 s2,写一个函数来判断 s2 是否包含 s1 的排列。
换句话说,第一个字符串的排列之一是第二个字符串的子串。
示例1:
输入: s1 = “ab” s2 = “eidbaooo”
输出: True
解释: s2 包含 s1 的排列之一 (“ba”).
示例2:
输入: s1= “ab” s2 = “eidboaoo”
输出: False
注意:
输入的字符串只包含小写字母
两个字符串的长度都在 [1, 10,000] 之间
//题目问a字符串里面是否含有b的排列。这等价于在a中找一个长度等于b且各字母出现频率相同的子字符串。
// 我们用一个sliding window模拟这个过程。初始化窗口为最左侧的且长度等于b字符串的长度的窗口。
// 在窗口滑动的过程中,我们可以减少部分运算,因为相邻的两个窗口的字母频率数组差别其实只有两个字母,
// 即前一个窗口的第一个字母和后一个窗口的最后一个字母处。
public class _3字符串的排列 {
public static boolean checkInclusion(String s1, String s2) {
if (s1.length()>s2.length()){
return false;
}
int length1=s1.length();
int length2=s2.length();
int[] frequency1=new int[26];
int[] frequency2=new int[26];
for (int i=0;i<length1;i++){
frequency1[s1.charAt(i)-'a']++;
frequency2[s2.charAt(i)-'a']++;
}
for (int i=0;i<length2-length1;i++){
if (compare(frequency1,frequency2)){
return true;
}
frequency2[s2.charAt(i)-'a']--;
frequency2[s2.charAt(length1+i)-'a']++;
}
return compare(frequency1,frequency2);
}
public static boolean compare(int [] frequency1,int[] frequency2){
for (int i=0;i<26;i++){
if (frequency1[i]!=frequency2[i]){
return false;
}
}
return true;
}
public static void main(String[] args) {
System.out.println(checkInclusion("ab","eidboaoo"));
}
}
4.字符串相乘
给定两个以字符串形式表示的非负整数 num1 和 num2,返回 num1 和 num2 的乘积,它们的乘积也表示为字符串形式。
示例 1:
输入: num1 = “2”, num2 = “3”
输出: “6”
示例 2:
输入: num1 = “123”, num2 = “456”
输出: “56088”
说明:
num1 和 num2 的长度小于110。
num1 和 num2 只包含数字 0-9。
num1 和 num2 均不以零开头,除非是数字 0 本身。
不能使用任何标准库的大数类型(比如 BigInteger)或直接将输入转换为整数来处理。
package LeetCode.byte_beating.字符串;
//限制条件是我们不能直接转化成整数,所以我们按照最基本的乘法手算算法来进行逐位计算
// 其中,两位数字相乘的结果(如3*4=12)具体是放在哪个位置是由相乘的两个数处于的数字中的位置所决定的
//
// 对于字符串中数字的获得,可以直接使用s.charAt(i)-'0',因为ascii码的特性
//
// 另外,结果要求首位没有空格,我们单独对这个要求进行一下处理即可
public class _4字符串相乘 {
public static String multiply(String num1, String num2) {
int length1=num1.length();
int length2=num2.length();
int[] output=new int[length1+length2];
for (int i=0;i<length1;i++){
for (int j=0;j<length2;j++){
int a=num1.charAt(length1-1-i)-'0';
int b=num2.charAt(length2-1-j)-'0';
mulAndAdd(output,i+j,a*b);
}
}
StringBuilder stringBuilder=new StringBuilder();
for (int i=0;i<output.length;i++){
stringBuilder.append(output[i]);
}
while (stringBuilder.length()>1){
if (stringBuilder.charAt(0)=='0'){
stringBuilder.deleteCharAt(0);
}else {
break;
}
}
return stringBuilder.toString();
}
public static void mulAndAdd(int [] output,int loc ,int num){
output[output.length-1-loc]+=num%10;
if (output[output.length-1-loc]>=10){
output[output.length-1-loc]-=10;
output[output.length-1-loc-1]+=1;
}
output[output.length-1-loc-1]+=num/10;
if (output[output.length-1-loc-1]>=10){
output[output.length-1-loc-1]-=10;
output[output.length-1-loc-2]+=1;
}
}
public static void main(String[] args) {
System.out.println(multiply("123","456"));
}
}
5.翻转字符串里的单词
给定一个字符串,逐个翻转字符串中的每个单词。
示例 1:
输入: “the sky is blue”
输出: “blue is sky the”
示例 2:
输入: " hello world! "
输出: “world! hello”
解释: 输入字符串可以在前面或者后面包含多余的空格,但是反转后的字符不能包括。
示例 3:
输入: “a good example”
输出: “example good a”
解释: 如果两个单词间有多余的空格,将反转后单词间的空格减少到只含一个。
说明:
无空格字符构成一个单词。
输入字符串可以在前面或者后面包含多余的空格,但是反转后的字符不能包括。
如果两个单词间有多余的空格,将反转后单词间的空格减少到只含一个。
进阶:
请选用 C 语言的用户尝试使用 O(1) 额外空间复杂度的原地解法。
package LeetCode.byte_beating.字符串;
//我们从字符串末端开始向前遍历字符串,在这个过程中,我们需要解决两个问题,
// 即,空格的处理和普通单词的处理。对于字符的处理,由于我们是从后向前遍历字符串,
// 而结果要求我们对单个的单词保持原状,所以我们采用reverse操作。对于空格的处理,
// 题目要求去掉荣誉空格,我们简单地用一个布尔遍历记录是否有空格即可。
//
// 最后,题目要求字符串前后端的空格要被消除,我们只用简单盯着字符串头尾是否是空格即可,如果是空格,就删除掉。
public class _5翻转字符串里的单词 {
public String reverseWords(String s) {
int length=s.length();
StringBuilder stringBuilder = new StringBuilder();
int index=length-1;
while (index>=0){
//连续空格只留一个
boolean addSpace=false;
while (index>=0&&s.charAt(index)==' '){
addSpace=true;
index--;
}
if (addSpace){
stringBuilder.append(" ");
}
//拼结单词
StringBuilder local=new StringBuilder();
while (index>=0&&s.charAt(index)!=' '){
local.append(s.charAt(index));
index--;
}
if (local.length()>0){
stringBuilder.append(local.reverse().toString());
}
}
//去除前面多余空格
while (stringBuilder.length()>0){
if (stringBuilder.charAt(0)==' '){
stringBuilder.deleteCharAt(0);
}else {
break;
}
}
//去除后面多余空格
while (stringBuilder.length()>0){
if (stringBuilder.charAt(stringBuilder.length()-1)==' '){
stringBuilder.deleteCharAt(stringBuilder.length()-1);
}else {
break;
}
}
return stringBuilder.toString();
}
}
6.简化路径
以 Unix 风格给出一个文件的绝对路径,你需要简化它。或者换句话说,将其转换为规范路径。
在 Unix 风格的文件系统中,一个点(.)表示当前目录本身;此外,两个点 (…) 表示将目录切换到上一级(指向父目录);两者都可以是复杂相对路径的组成部分。更多信息请参阅:Linux / Unix中的绝对路径 vs 相对路径
请注意,返回的规范路径必须始终以斜杠 / 开头,并且两个目录名之间必须只有一个斜杠 /。最后一个目录名(如果存在)不能以 / 结尾。此外,规范路径必须是表示绝对路径的最短字符串。
示例 1:
输入:"/home/"
输出:"/home"
解释:注意,最后一个目录名后面没有斜杠。
示例 2:
输入:"/…/"
输出:"/"
解释:从根目录向上一级是不可行的,因为根是你可以到达的最高级。
示例 3:
输入:"/home//foo/"
输出:"/home/foo"
解释:在规范路径中,多个连续斜杠需要用一个斜杠替换。
示例 4:
输入:"/a/./b/…/…/c/"
输出:"/c"
示例 5:
输入:"/a/…/…/b/…/c//.//"
输出:"/c"
示例 6:
输入:"/a//bc/d//././/…"
输出:"/a/b/c"
package LeetCode.byte_beating.字符串;
import java.util.*;
//我们先总结一下简化路径的要求: 1.字符串末尾没有斜杠 2.多个连续斜杠用一个斜杠代替
// 3.由根目录不能向上一级,所以碰到这种情况时,返回结果认为依然是根目录 4.一个点.表示当前目录 5.两个连续的点..表示父目录
//
// 分析要求后,我们决定采用栈的数据结构,将每一层级路径的解析抽象为入栈和出栈操作。由于要求中的第四条与第二条,
// 我们在处理路径时可能遇到需要跳过(即不入栈)的操作,所以我们使用hashset存储需要跳过的字符,这样在判断是否需要跳过时,我们只需要O(1)即可完成
public class _6简化路径 {
public static String simplifyPath(String path) {
Deque<String> stack = new LinkedList<>();
//Stack<String> stack = new Stack<String>();
Set<String> set = new HashSet<>();
set.add(".");
set.add("..");
set.add("");
String[] skip=path.split("/");
for (String dir:skip){
if (dir.equals("..")){
stack.pop();
}else if (!set.contains(dir)){
stack.push(dir);
}
}
String res="";
// for (String s:stack){
// res="/"+s+res;
// }
while (!stack.isEmpty()){
res="/"+stack.pop()+res;
}
if (res.equals("")){
return "/";
}
return res;
}
public static void main(String[] args) {
System.out.println(simplifyPath("/a//bc/d//././/.."));
}
}
7.复原IP地址
给定一个只包含数字的字符串,复原它并返回所有可能的 IP 地址格式。
有效的 IP 地址正好由四个整数(每个整数位于 0 到 255 之间组成),整数之间用 ‘.’ 分隔。
示例:
输入: “25525511135”
输出: [“255.255.11.135”, “255.255.111.35”]
package LeetCode.byte_beating.字符串;
import java.util.*;
//题目要求我们返回所有的有效的ip地址,我们可以将这个问题分为两个子问题分别进行解决。
// 问题1:如何将输入字符串分割为所有可能有效的四部分;问题2:如何判断被分割好的字符串是否有效
//
// 问题2很好解决,只需要验证每部分的长度以及大小符合ip地址的定义即可
//
// 问题1我们可以用回溯法来枚举所有的可能分割;回溯法的另一个好处是可以减少部分运算。
// 例如我们分割到了第二部分,发现第二部分的分割结果只有两种有效的方法(如输入:255332421,我们的第一部分分割到255,
// 第二部分只能分割为3或者33),这样可以避免计算第二部分分割为332的所有计算,因为一个有效的ip地址必定是四个部分全部有效的
public class _7复原IP地址 {
static List<String> res = new ArrayList<String>();
static ArrayList<String> single = new ArrayList<>();
public static List<String> restoreIpAddresses(String s) {
if (s.length()>12){
return res;
}
allPossible(4,s);
return res;
}
private static void allPossible(int times, String s) {
if (times==1){
if (s.length()<=3&&s.length()>0&&Integer.parseInt(s)<=255&&Integer.parseInt(s)>=0){
if (!(s.length()>1&&s.charAt(0)=='0')){
single.add(s);
System.out.println("end:single: "+s);
res.add(stringArrayToString(single));
single.remove(s);
}
}
}else {
int endLength = Math.min(s.length(),3);
if (s.length()!=0&&s.charAt(0)=='0'){
endLength=1;
}
for (int i=1;i<=endLength;i++){
String thisPart = s.substring(0,i);
if (Integer.parseInt(thisPart)<=255&&Integer.parseInt(thisPart)>=0){
single.add(thisPart+".");
System.out.println("single:"+single.toString());
allPossible(times-1,s.substring(i,s.length()));
single.remove(single.size()-1);
}
}
}
}
private static String stringArrayToString(ArrayList<String> single) {
StringBuilder stringBuilder = new StringBuilder();
for (String s:single){
stringBuilder.append(s);
}
return stringBuilder.toString();
}
public static void main(String[] args) {
System.out.println(restoreIpAddresses("25525511135").toString());
}
}
二、数组与排序
1.三数之和
给你一个包含 n 个整数的数组 nums,判断 nums 中是否存在三个元素 a,b,c ,使得 a + b + c = 0 ?请你找出所有满足条件且不重复的三元组。
注意:答案中不可以包含重复的三元组。
示例:
给定数组 nums = [-1, 0, 1, 2, -1, -4],
满足要求的三元组集合为:
[
[-1, 0, 1],
[-1, -1, 2]
]
package LeetCode.byte_beating.数组与排序;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
//单纯的暴力解法包含大量的重复运算(重复的求和计算),我们通过对数组的排序,减少这种重复计算;
// 具体来讲:我们首先将数组排序,然后遍历数组的每一个index=i,对于每个值(我们记这个指为a),
// 我们需要找到另外找到数组中的两个数,使得他们的和等于一个定值(sum-a)。
// 为了寻找这个定值和,我们从数组的其余部分寻找,我们初始化左指针为index=i+1,右指针为index=length-1;
// 由于数组是排序过的,所以如果nums[i+1]+nums[length-1]<sum-a,我们可以知道,nums[i+1]和
// index=i+2~length-1区间内的任何数的和都不可能等于sum-a。由此我们可以大大简化计算。
public class _1三数之和 {
public static List<List<Integer>> threeSum(int[] nums) {
List<List<Integer>> list = new ArrayList<>();
int length=nums.length;
if (length <= 2) {
return list;
}
Arrays.sort(nums);
for (int i=0;i<length-2;i++){
if (i>0&&(nums[i]==nums[i-1])){
continue;
}
int left=i+1;
int right=length-1;
while (left<right){
int sum=nums[i]+nums[left]+nums[right];
if (sum==0){
list.add(Arrays.asList(nums[i],nums[left],nums[right]));
left++;
right--;
while (left<right&&nums[left]==nums[left-1]){
left++;
}
while (left<right&&nums[right]==nums[right+1]){
right--;
}
}else if (sum>0){
right--;
}else {
left++;
}
}
}
return list;
}
public static void main(String[] args) {
int[] a =new int[]{-1, 0, 1, 2, -1, -4};
System.out.println(threeSum(a).toString());
}
}
2.岛屿的最大面积
给定一个包含了一些 0 和 1 的非空二维数组 grid 。
一个 岛屿 是由一些相邻的 1 (代表土地) 构成的组合,这里的「相邻」要求两个 1 必须在水平或者竖直方向上相邻。你可以假设 grid 的四个边缘都被 0(代表水)包围着。
找到给定的二维数组中最大的岛屿面积。(如果没有岛屿,则返回面积为 0 。)
示例 1:
[[0,0,1,0,0,0,0,1,0,0,0,0,0],
[0,0,0,0,0,0,0,1,1,1,0,0,0],
[0,1,1,0,1,0,0,0,0,0,0,0,0],
[0,1,0,0,1,1,0,0,1,0,1,0,0],
[0,1,0,0,1,1,0,0,1,1,1,0,0],
[0,0,0,0,0,0,0,0,0,0,1,0,0],
[0,0,0,0,0,0,0,1,1,1,0,0,0],
[0,0,0,0,0,0,0,1,1,0,0,0,0]]
对于上面这个给定矩阵应返回 6。注意答案不应该是 11 ,因为岛屿只能包含水平或垂直的四个方向的 1 。
示例 2:
[[0,0,0,0,0,0,0,0]]
对于上面这个给定的矩阵, 返回 0。
注意: 给定的矩阵grid 的长度和宽度都不超过 50。
package LeetCode.byte_beating.数组与排序;
//用visit数组记录各个节点是否已经被遍历过。用深度优先遍历并且记录最大岛屿面积即可
public class _2岛屿的最大面积 {
public static int maxAreaOfIsland(int[][] grid) {
int rows = grid.length;
int cols = grid[0].length;
int[][] visit = new int[rows][cols];
int count = 0;
for (int i = 0; i < rows; i++) {
for (int j = 0; j < cols; j++) {
if (visit[i][j] == 1) {
continue;
}
if (grid[i][j] == 1) {
visit[i][j] = 1;
count = Math.max(count, calArea(i, j, grid, visit));
}
}
}
return count;
}
private static int calArea(int i, int j,int[][] grid,int[][] visit) {
int output = 1;
if (i-1>=0&&grid[i-1][j]==1&&visit[i-1][j]==0){
visit[i-1][j] = 1;
grid[i-1][j] = 0;
output+=calArea(i-1,j,grid,visit);
}
if (i+1<grid.length&&grid[i+1][j]==1&&visit[i+1][j]==0){
visit[i+1][j] = 1;
grid[i+1][j] = 0;
output+=calArea(i+1,j,grid,visit);
}
if (j-1>=0&&grid[i][j-1]==1&&visit[i][j-1]==0){
visit[i][j-1] = 1;
grid[i][j-1] = 0;
output+=calArea(i,j-1,grid,visit);
}
if (j+1<grid[0].length&&grid[i][j+1]==1&&visit[i][j+1]==0){
visit[i][j+1] = 1;
grid[i][j+1] = 0;
output+=calArea(i,j+1,grid,visit);
}
return output;
}
public static void main(String[] args) {
int [][] a=new int[][]{{0,0,1,0,0,0,0,1,0,0,0,0,0},
{0,0,0,0,0,0,0,1,1,1,0,0,0},
{0,1,1,0,1,0,0,0,0,0,0,0,0},
{0,1,0,0,1,1,0,0,1,0,1,0,0},
{0,1,0,0,1,1,0,0,1,1,1,0,0},
{0,0,0,0,0,0,0,0,0,0,1,0,0},
{0,0,0,0,0,0,0,1,1,1,0,0,0},
{0,0,0,0,0,0,0,1,1,0,0,0,0}};
System.out.println(maxAreaOfIsland(a));
}
}
3.搜索旋转排序数组
假设按照升序排序的数组在预先未知的某个点上进行了旋转。
( 例如,数组 [0,1,2,4,5,6,7] 可能变为 [4,5,6,7,0,1,2] )。
搜索一个给定的目标值,如果数组中存在这个目标值,则返回它的索引,否则返回 -1 。
你可以假设数组中不存在重复的元素。
你的算法时间复杂度必须是 O(log n) 级别。
示例 1:
输入: nums = [4,5,6,7,0,1,2], target = 0
输出: 4
示例 2:
输入: nums = [4,5,6,7,0,1,2], target = 3
输出: -1
package LeetCode.byte_beating.数组与排序;
//遍历数组,找到旋转点。旋转点两侧为有序数组,用二分查找即可
public class _3搜索旋转排序数组 {
public static int biSearch(int i, int index,int[] nums,int target){
int left=i;
int right=index;
while (left<=right){
int mid=(left+right)/2;
if (nums[mid]==target){
return mid;
}else if (nums[mid]>target){
right=mid-1;
}else {
left=mid+1;
}
}
return -1;
}
public static int search(int[] nums, int target){
int length=nums.length;
int output=-1;
int index=0;
for (int i=0;i<length;i++){
if (nums[i]>nums[i+1]){
index=i;
break;
}
}
output=biSearch(0,index,nums,target);
if (output!=-1){
return output;
}else {
return biSearch(index+1,length-1,nums,target);
}
}
public static void main(String[] args) {
int [] a= new int[]{4,5,6,7,0,1,2};
System.out.println(search(a,0));
}
}
4.最长连续递增序列
给定一个未经排序的整数数组,找到最长且连续的的递增序列,并返回该序列的长度。
示例 1:
输入: [1,3,5,4,7]
输出: 3
解释: 最长连续递增序列是 [1,3,5], 长度为3。
尽管 [1,3,5,7] 也是升序的子序列, 但它不是连续的,因为5和7在原数组里被4隔开。
示例 2:
输入: [2,2,2,2,2]
输出: 1
解释: 最长连续递增序列是 [2], 长度为1。
注意:数组长度不会超过10000。
package LeetCode.byte_beating.数组与排序;
//滑动窗口法;在窗口右外侧的数字如果比窗口的最右侧元素大,则扩展窗口。
// 否则则将窗口移动到此处并且将窗口长度初始化为0;这期间记录产生的窗口的最大长度
public class _4最长连续递增序列 {
public static int findLengthOfLCIS(int[] nums) {
int output = 1;
int length = nums.length;
if (length==0){
return 0;
}
int left = 0;
int right = 0;
int local = 0;
while (right<length){
System.out.println("right="+right);
if (right==left||nums[right]>nums[right-1]){
local++;
right++;
output=Math.max(local,output);
}else {
left=right;
local=0;
}
}
return output;
}
public static void main(String[] args) {
int [] a=new int[]{1,3,5,4,7};
System.out.println(findLengthOfLCIS(a));
}
}
5.数组中的第K个最大元素
在未排序的数组中找到第 k 个最大的元素。请注意,你需要找的是数组排序后的第 k 个最大的元素,而不是第 k 个不同的元素。
示例 1:
输入: [3,2,1,5,6,4] 和 k = 2
输出: 5
示例 2:
输入: [3,2,3,1,2,4,5,5,6] 和 k = 4
输出: 4
说明:
你可以假设 k 总是有效的,且 1 ≤ k ≤ 数组的长度。
package LeetCode.byte_beating.数组与排序;
//堆排序进行K次即可,典型的最大K个数或者第K大数问题。
//
// 堆排序的思路为,通过每次构建最大或最小堆,在logn时间内得到最大或最小值。详见下篇排序专题文章。
public class _5数组中的第K个最大元素 {
public static int findKthLargest(int[] nums, int k) {
int output = 0;
for (int i=0;i<k;i++){
output=makeHeap(nums,nums.length-i-1);
}
return output;
}
//堆排
private static int makeHeap(int[] input, int index) {
for (int i=index;i>=0;i--){
int rootIndex = 0;
if (i%2==0){
rootIndex = (i-2)/2;
}else {
rootIndex = (i-1)/2;
}
if ((rootIndex>=0&&input[rootIndex]<input[i])){
exchange(i,rootIndex,input);
}
}
exchange(0,index,input);
return input[index];
}
private static void exchange(int i, int i1, int[] input) {
int mid=input[i];
input[i]=input[i1];
input[i1]=mid;
}
public static void main(String[] args) {
int [] a=new int[]{3,2,3,1,2,4,5,5,6};
System.out.println(findKthLargest(a,4));
}
}
6.最长连续序列
给定一个未排序的整数数组,找出最长连续序列的长度。
要求算法的时间复杂度为 O(n)。
示例:
输入: [100, 4, 200, 1, 3, 2]
输出: 4
解释: 最长连续序列是 [1, 2, 3, 4]。它的长度为 4。
package LeetCode.byte_beating.数组与排序;
import java.util.HashSet;
//HashSet记录所有出现过的数字,再次遍历并寻找连续序列;注意,由于我们不想被重复的序列干扰,
// 比如{1,2,3,4}中,我们可能遍历到1和2时都会发现递增序列,所以我们指定,
// 只有在遍历到递增序列某一端(最大的数或者最小的数)时才开始计算序列长度
public class _6最长连续序列 {
public int longestConsecutive(int[] nums) {
if (nums.length==0){
return 0;
}
int output=1;
HashSet<Integer> set = new HashSet<>();
for (int i:nums){
set.add(i);
}
for (int i:nums){
if (set.contains(i+1)){
continue;
}else if (set.contains(i-1)){
int local=i;
int count=1;
while (set.contains(local-1)){
count++;
local--;
}
output=Math.max(count,output);
}
}
return output;
}
}
7.第k个排列
给出集合 [1,2,3,…,n],其所有元素共有 n! 种排列。
按大小顺序列出所有排列情况,并一一标记,当 n = 3 时, 所有排列如下:
“123”
“132”
“213”
“231”
“312”
“321”
给定 n 和 k,返回第 k 个排列。
说明:
给定 n 的范围是 [1, 9]。
给定 k 的范围是[1, n!]。
示例 1:
输入: n = 3, k = 3
输出: “213”
示例 2:
输入: n = 4, k = 9
输出: “2314”
package LeetCode.byte_beating.数组与排序;
import java.util.LinkedList;
//https://www.optbbs.com/thread-4629585-1-1.html
public class _7第k个排列 {
StringBuilder stringBuilder;
public String getPermutation(int n, int k) {
stringBuilder = new StringBuilder();
LinkedList<Integer> arrayList = new LinkedList<>();
for (int i=1;i<=n;i++){
arrayList.add(i);
}
buildString(arrayList,k);
return stringBuilder.toString();
}
private void buildString(LinkedList<Integer> arrayList, int k) {
if (k==0){
return;
}
if (k>cal(arrayList.size())){
return;
}
if (arrayList.size()==0){
return;
}
int length = arrayList.size();
if (length==2){
if (k==1){
stringBuilder.append(arrayList.get(0));
arrayList.remove(0);
}else if (k==2){
stringBuilder.append(arrayList.get(1));
arrayList.remove(1);
}
buildString(arrayList,1);
return;
}
if (length==1){
stringBuilder.append(arrayList.get(0));
arrayList.remove(0);
return;
}
int index = ((k-1)/cal(length-1));
stringBuilder.append(arrayList.get(index));
arrayList.remove(index);
buildString(arrayList,k-(index)*cal(length-1));
}
private int cal(int i) {
int output = 1;
while (i>0){
output*=i;
i--;
}
return output;
}
}
8.朋友圈
班上有 N 名学生。其中有些人是朋友,有些则不是。他们的友谊具有是传递性。如果已知 A 是 B 的朋友,B 是 C 的朋友,那么我们可以认为 A 也是 C 的朋友。所谓的朋友圈,是指所有朋友的集合。
给定一个 N * N 的矩阵 M,表示班级中学生之间的朋友关系。如果M[i][j] = 1,表示已知第 i 个和 j 个学生互为朋友关系,否则为不知道。你必须输出所有学生中的已知的朋友圈总数。
示例 1:
输入:
[[1,1,0],
[1,1,0],
[0,0,1]]
输出: 2
说明:已知学生0和学生1互为朋友,他们在一个朋友圈。
第2个学生自己在一个朋友圈。所以返回2。
示例 2:
输入:
[[1,1,0],
[1,1,1],
[0,1,1]]
输出: 1
说明:已知学生0和学生1互为朋友,学生1和学生2互为朋友,所以学生0和学生2也是朋友,所以他们三个在一个朋友圈,返回1。
注意:
N 在[1,200]的范围内。
对于所有学生,有M[i][i] = 1。
如果有M[i][j] = 1,则有M[j][i] = 1。
package LeetCode.byte_beating.数组与排序;
//DFS并且记录朋友圈个数即可
public class _8朋友圈 {
int count;
public int findCircleNum(int[][] M) {
count = 0;
int n = M.length;
int[] visited = new int[n];
for (int i=0;i<n;i++){
if (visited[i]==1){
continue;
}else {
count++;
visited[i]=1;
dfs(i,M,visited);
}
}
return count;
}
private void dfs(int i, int[][] m, int[] visited) {
for (int j=0;j<m.length;j++){
if (i==j||visited[j]==1||m[i][j]==0){
continue;
}else {
visited[j]=1;
dfs(j,m,visited);
}
}
}
}
9.合并区间
给出一个区间的集合,请合并所有重叠的区间。
示例 1:
输入: [[1,3],[2,6],[8,10],[15,18]]
输出: [[1,6],[8,10],[15,18]]
解释: 区间 [1,3] 和 [2,6] 重叠, 将它们合并为 [1,6].
示例 2:
输入: [[1,4],[4,5]]
输出: [[1,5]]
解释: 区间 [1,4] 和 [4,5] 可被视为重叠区间。
package LeetCode.byte_beating.数组与排序;
import java.util.ArrayList;
public class _9合并区间 {
public static int[][] merge(int[][] intervals) {
if (intervals.length==0){
return new int[0][0];
}
mergeSort(intervals,0,intervals.length-1);
for (int[] inter:intervals){
// System.out.println(Arrays.toString(inter));
// System.out.println("###########");
}
ArrayList<Integer> arrayList = new ArrayList<>();
for (int i=0;i<intervals.length;i++){
arrayList.add(i);
int local = i;
// System.out.println("add:"+i);
while (i<intervals.length-1&&intervals[local][1]>=intervals[i+1][0]){
intervals[local][1] = Math.max(intervals[local][1],intervals[i+1][1]);
i++;
}
}
int[][] output = new int[arrayList.size()][2];
for (int i =0;i<arrayList.size();i++){
output[i][0] = intervals[arrayList.get(i)][0];
output[i][1] = intervals[arrayList.get(i)][1];
}
return output;
}
private static void mergeSort(int[][] intervals, int left, int right) {
if (left==right){
return;
}else {
int M = (right+left)/2;
mergeSort(intervals,left,M);
mergeSort(intervals,M+1,right);
mergeAll(intervals,left,M+1,right);
}
}
private static void mergeAll(int[][] intervals, int left, int m, int right) {
int[][] leftNums = new int[m-left][2];
int[][] rightNums = new int[right-m+1][2];
for (int i=left;i<m;i++){
leftNums[i-left][0] = intervals[i][0];
leftNums[i-left][1] = intervals[i][1];
}
for (int i=m;i<=right;i++){
rightNums[i-m][0] = intervals[i][0];
rightNums[i-m][1] = intervals[i][1];
}
int i = 0;
int j = 0;
int k = left;
while (i<leftNums.length&&j<rightNums.length){
if (leftNums[i][0]<rightNums[j][0]){
intervals[k][0] = leftNums[i][0];
intervals[k][1] = leftNums[i][1];
i++;
}else {
intervals[k][0] = rightNums[j][0];
intervals[k][1] = rightNums[j][1];
j++;
}
k++;
}
while (i<leftNums.length){
intervals[k][0] = leftNums[i][0];
intervals[k][1] = leftNums[i][1];
i++;
k++;
}
while (j<rightNums.length){
intervals[k][0] = rightNums[j][0];
intervals[k][1] = rightNums[j][1];
k++;
j++;
}
}
public static void main(String[] args) {
int [][] a= new int[][]{{1,4},{4,5}};
int[][] s=merge(a);
for (int i=0;i<s.length;i++){
for (int j=0; j<s[0].length;j++){
System.out.print(s[i][j]+" ");
}
System.out.println();
}
}
}
10.接雨水
给定 n 个非负整数表示每个宽度为 1 的柱子的高度图,计算按此排列的柱子,下雨之后能接多少雨水。
上面是由数组 [0,1,0,2,1,0,1,3,2,1,2,1] 表示的高度图,在这种情况下,可以接 6 个单位的雨水(蓝色部分表示雨水)。 感谢 Marcos 贡献此图。
示例:
输入: [0,1,0,2,1,0,1,3,2,1,2,1]
输出: 6
package LeetCode.byte_beating.数组与排序;
//根据对接水这个动作的分析,我们发现,储存水的地方能储存多少的水与此处的左侧右侧高度有关;
// 注意,这里说的“左侧右侧”高度是指当前index下左侧的最高的高度和右侧的最高的高度。
// 所以我们计算每个节点对应的左侧高度最大值和右侧高度最大值,这样通过计算可以得知每一个节点上方能存储多少水。
public class _10接雨水 {
public static int trap(int[] height) {
int count =0;
int length=height.length;
if (length<2){
return 0;
}
int[] left=new int[length];
int[] rigth=new int[length];
left[0]=height[0];
for (int i=1;i<=length-1;i++){
left[i]=Math.max(left[i-1],height[i]);
}
rigth[length-1]=height[length-1];
for (int i=length-2;i>=0;i--){
rigth[i]=Math.max(rigth[i+1],height[i]);
}
for (int i=1;i<length-1;i++){
count+=Math.min(left[i],rigth[i])-height[i];
}
return count;
}
public static void main(String[] args) {
int[] a= new int[]{0,1,0,2,1,0,1,3,2,1,2,1};
System.out.println(trap(a));
}
}
三、链表与树
1、合并两个有序链表
将两个升序链表合并为一个新的 升序 链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。
示例:
输入:1->2->4, 1->3->4
输出:1->1->2->3->4->4
package LeetCode.byte_beating.链表与树;
//新建一个带有哨兵结点的链表,依次比较两个有序链表的结点值,将较小值的结点插入到新链表后面。直到其中一个比较完毕,
// 将另一个链表剩余的结点全部放到新链表最后面即可。最后,可以删除哨兵结点,或者直接返回哨兵结点后第一个结点指针。
public class _1合并两个有序链表 {
public static class ListNode {
int val;
ListNode next;
ListNode() {}
ListNode(int val) { this.val = val; }
ListNode(int val, ListNode next) { this.val = val; this.next = next; }
}
public static ListNode mergeTwoLists(ListNode l1, ListNode l2) {
ListNode temp1=l1;
ListNode temp2=l2;
ListNode ret=new ListNode(0);
ListNode ret1=ret;
while (temp1!=null&&temp2!=null){
if (temp1.val>temp2.val){
ret.next=temp2;
temp2=temp2.next;
}else {
ret.next=temp1;
temp1=temp1.next;
}
ret=ret.next;
}
if (temp1!=null&&temp2==null){
ret.next=temp1;
}
if (temp2!=null&&temp1==null){
ret.next=temp2;
}
return ret1.next;
}
public static void main(String[] args) {
ListNode h1=new ListNode(1);
h1.next=new ListNode(2);
h1.next.next=new ListNode(4);
ListNode h2=new ListNode(1);
h2.next=new ListNode(3);
h2.next.next=new ListNode(4);
ListNode a=mergeTwoLists(h1,h2);
while (a!=null){
System.out.println(a.val);
a=a.next;
}
}
}
2、反转链表
反转一个单链表。
示例:
输入: 1->2->3->4->5->NULL
输出: 5->4->3->2->1->NULL
进阶:
你可以迭代或递归地反转链表。你能否用两种方法解决这道题?
package LeetCode.byte_beating.链表与树;
public class _2反转链表 {
public static class ListNode {
int val;
ListNode next;
ListNode() {}
ListNode(int val) { this.val = val; }
ListNode(int val, ListNode next) { this.val = val; this.next = next; }
}
public ListNode reverseList(ListNode head) {
if (head==null||head.next==null){
return head;
}
ListNode pre=null;
ListNode curr=head;
while (curr!=null){
ListNode next=curr.next;
curr.next=pre;
pre=curr;
curr=next;
}
return pre;
}
}
3、两数相加
给出两个 非空 的链表用来表示两个非负的整数。其中,它们各自的位数是按照 逆序 的方式存储的,并且它们的每个节点只能存储 一位 数字。
如果,我们将这两个数相加起来,则会返回一个新的链表来表示它们的和。
您可以假设除了数字 0 之外,这两个数都不会以 0 开头。
示例:
输入:(2 -> 4 -> 3) + (5 -> 6 -> 4)
输出:7 -> 0 -> 8
原因:342 + 465 = 807
package LeetCode.byte_beating.链表与树;
//我们使用变量来跟踪进位,并从包含最低有效位的表头开始模拟逐位相加的过程。
// 就像你在纸上计算两个数字的和那样,我们首先从最低有效位也就是列表 l1l1 和 l2l2 的表头开始相加。
// 由于每位数字都应当处于 0…9 的范围内,我们计算两个数字的和时可能会出现“溢出”。
// 例如,5+7=12。在这种情况下,我们会将当前位的数值设置为 22,
// 并将进位 carry = 1carry=1 带入下一次迭代。进位 carrycarry 必定是 00 或 11,
// 这是因为两个数字相加(考虑到进位)可能出现的最大和为9+9+1=19.
//伪代码如下:
//
//将当前结点初始化为返回列表的哑结点。
//将进位 carrycarry 初始化为 00。
//将 pp 和 qq 分别初始化为列表 l1l1 和 l2l2 的头部。
//遍历列表 l1l1 和 l2l2 直至到达它们的尾端。
//将 xx 设为结点 pp 的值。如果 pp 已经到达 l1l1 的末尾,则将其值设置为 00。
//将 yy 设为结点 qq 的值。如果 qq 已经到达 l2l2 的末尾,则将其值设置为 00。
//设定 sum = x + y + carrysum=x+y+carry。
//更新进位的值,carry = sum / 10carry=sum/10。
//创建一个数值为 (sum \bmod 10)(summod10) 的新结点,并将其设置为当前结点的下一个结点,然后将当前结点前进到下一个结点。
//同时,将 pp 和 qq 前进到下一个结点。
//检查 carry = 1carry=1 是否成立,如果成立,则向返回列表追加一个含有数字 11 的新结点。
//返回哑结点的下一个结点。
//请注意,我们使用哑结点来简化代码。如果没有哑结点,则必须编写额外的条件语句来初始化表头的值。
public class _3两数相加 {
public static class ListNode {
int val;
ListNode next;
ListNode() {}
ListNode(int val) { this.val = val; }
ListNode(int val, ListNode next) { this.val = val; this.next = next; }
}
public ListNode addTwoNumbers(ListNode l1, ListNode l2) {
ListNode dummyHead = new ListNode(0);
ListNode p = l1, q = l2, curr = dummyHead;
int carry = 0;
while (p != null || q != null) {
int x = (p != null) ? p.val : 0;
int y = (q != null) ? q.val : 0;
int sum = carry + x + y;
carry = sum / 10;
curr.next = new ListNode(sum % 10);
curr = curr.next;
if (p != null) p = p.next;
if (q != null) q = q.next;
}
if (carry > 0) {
curr.next = new ListNode(carry);
}
return dummyHead.next;
}
}
4、排序链表
在 O(n log n) 时间复杂度和常数级空间复杂度下,对链表进行排序。
示例 1:
输入: 4->2->1->3
输出: 1->2->3->4
示例 2:
输入: -1->5->3->4->0
输出: -1->0->3->4->5
package LeetCode.byte_beating.链表与树;
//归并排序(递归法)
//题目要求时间空间复杂度分别为O(nlogn)O(nlogn)和O(1)O(1),根据时间复杂度我们自然想到二分法,从而联想到归并排序;
//
//对数组做归并排序的空间复杂度为 O(n)O(n),分别由新开辟数组O(n)O(n)和递归函数调用O(logn)O(logn)组成,而根据链表特性:
//
//数组额外空间:链表可以通过修改引用来更改节点顺序,无需像数组一样开辟额外空间;
//递归额外空间:递归调用函数将带来O(logn)O(logn)的空间复杂度,因此若希望达到O(1)O(1)空间复杂度,则不能使用递归。
//通过递归实现链表归并排序,有以下两个环节:
//
//分割 cut 环节: 找到当前链表中点,并从中点将链表断开(以便在下次递归 cut 时,链表片段拥有正确边界);
//我们使用 fast,slow 快慢双指针法,奇数个节点找到中点,偶数个节点找到中心左边的节点。
//找到中点 slow 后,执行 slow.next = None 将链表切断。
//递归分割时,输入当前链表左端点 head 和中心节点 slow 的下一个节点 tmp(因为链表是从 slow 切断的)。
//cut 递归终止条件: 当head.next == None时,说明只有一个节点了,直接返回此节点。
//合并 merge 环节: 将两个排序链表合并,转化为一个排序链表。
//双指针法合并,建立辅助ListNode h 作为头部。
//设置两指针 left, right 分别指向两链表头部,比较两指针处节点值大小,由小到大加入合并链表头部,指针交替前进,直至添加完两个链表。
//返回辅助ListNode h 作为头部的下个节点 h.next。
//时间复杂度 O(l + r),l, r 分别代表两个链表长度。
//当题目输入的 head == None 时,直接返回None。
public class _4排序链表 {
public static class ListNode {
int val;
ListNode next;
ListNode() {}
ListNode(int val) { this.val = val; }
ListNode(int val, ListNode next) { this.val = val; this.next = next; }
}
public static ListNode sortList(ListNode head) {
if (head == null || head.next == null)
return head;
ListNode fast = head.next, slow = head;
while (fast != null && fast.next != null) {
slow = slow.next;
fast = fast.next.next;
}
ListNode tmp = slow.next;
slow.next = null;
ListNode left = sortList(head);
ListNode right = sortList(tmp);
ListNode h = new ListNode(0);
ListNode res = h;
while (left != null && right != null) {
if (left.val < right.val) {
h.next = left;
left = left.next;
} else {
h.next = right;
right = right.next;
}
h = h.next;
}
h.next = left != null ? left : right;
return res.next;
}
}
5、环形链表2
给定一个链表,返回链表开始入环的第一个节点。 如果链表无环,则返回 null。
为了表示给定链表中的环,我们使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。 如果 pos 是 -1,则在该链表中没有环。
说明:不允许修改给定的链表。
示例 1:
输入:head = [3,2,0,-4], pos = 1
输出:tail connects to node index 1
解释:链表中有一个环,其尾部连接到第二个节点。
示例 2:
输入:head = [1,2], pos = 0
输出:tail connects to node index 0
解释:链表中有一个环,其尾部连接到第一个节点。
示例 3:
输入:head = [1], pos = -1
输出:no cycle
解释:链表中没有环。
进阶:
你是否可以不用额外空间解决此题?
package LeetCode.byte_beating.链表与树;
import 一._55链表中环的入口节点;
/*
定理:两个指针一个fast、一个slow同时从一个链表的头部出发
fast一次走2步,slow一次走一步,如果该链表有环,两个指针必然在环内相遇
此时只需要把其中的一个指针重新指向链表头部,另一个不变(还在环内),
这次两个指针一次走一步,相遇的地方就是入口节点。
*/
public class _5环形链表2 {
public static class ListNode {
int val;
ListNode next;
ListNode() {}
ListNode(int val) { this.val = val; }
ListNode(int val, ListNode next) { this.val = val; this.next = next; }
}
public ListNode detectCycle(ListNode pHead) {
//空链表或者一个节点肯定没有环
if(pHead == null || pHead.next == null)
return null;
ListNode fast = pHead;
ListNode slow = pHead;
//fast一次跳两个节点,slow一次跳一个节点
//若有环,一定相遇,在环的某个节点停住
while(fast!=null && fast.next!=null){
fast = fast.next.next;
slow = slow.next;
if(fast == slow){
//一个重新指向头,一个不动,相遇点就是入口节点
fast = pHead;
while(fast != slow){
fast = fast.next;
slow = slow.next;
}
return fast;
}
}
return null;
}
}
6、相交链表
编写一个程序,找到两个单链表相交的起始节点。
如下面的两个链表:
在节点 c1 开始相交。
示例 1:
输入:intersectVal = 8, listA = [4,1,8,4,5], listB = [5,0,1,8,4,5], skipA = 2, skipB = 3
输出:Reference of the node with value = 8
输入解释:相交节点的值为 8 (注意,如果两个链表相交则不能为 0)。从各自的表头开始算起,链表 A 为 [4,1,8,4,5],链表 B 为 [5,0,1,8,4,5]。在 A 中,相交节点前有 2 个节点;在 B 中,相交节点前有 3 个节点。
示例 2:
输入:intersectVal = 2, listA = [0,9,1,2,4], listB = [3,2,4], skipA = 3, skipB = 1
输出:Reference of the node with value = 2
输入解释:相交节点的值为 2 (注意,如果两个链表相交则不能为 0)。从各自的表头开始算起,链表 A 为 [0,9,1,2,4],链表 B 为 [3,2,4]。在 A 中,相交节点前有 3 个节点;在 B 中,相交节点前有 1 个节点。
示例 3:
输入:intersectVal = 0, listA = [2,6,4], listB = [1,5], skipA = 3, skipB = 2
输出:null
输入解释:从各自的表头开始算起,链表 A 为 [2,6,4],链表 B 为 [1,5]。由于这两个链表不相交,所以 intersectVal 必须为 0,而 skipA 和 skipB 可以是任意值。
解释:这两个链表不相交,因此返回 null。
注意:
如果两个链表没有交点,返回 null.
在返回结果后,两个链表仍须保持原有的结构。
可假定整个链表结构中没有循环。
程序尽量满足 O(n) 时间复杂度,且仅用 O(1) 内存。
package LeetCode.byte_beating.链表与树;
//先求两个链表的长度,让长的先走比短的多的节点数
//在让两个链表同时向后走,直到遇到相等的点就返回
public class _6相交链表 {
public static class ListNode {
int val;
ListNode next;
ListNode() {}
ListNode(int val) { this.val = val; }
ListNode(int val, ListNode next) { this.val = val; this.next = next; }
}
public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
int length1=0;
int length2=0;
ListNode h1=headA;
ListNode h2=headB;
while (h1!=null){
length1++;
h1=h1.next;
}
while (h2!=null){
length2++;
h1=h2.next;
}
ListNode h3=headA;
ListNode h4=headB;
if (length1>length2){
int cha=length1-length2;
while (cha>=0){
h3=h3.next;
}
}else {
int cha=length2-length1;
while (cha>=0){
h4=h4.next;
}
}
while (h3!=null&&h4!=null&&h3.val!=h4.val){
h3=h3.next;
h4=h4.next;
}
return h3;
}
}
7、合并K个排序链表
合并 k 个排序链表,返回合并后的排序链表。请分析和描述算法的复杂度。
示例:
输入:
[
1->4->5,
1->3->4,
2->6
]
输出: 1->1->2->3->4->4->5->6
package LeetCode.byte_beating.链表与树;
//合并两个有序链表可以很容易地想到归并排序的方法。合并K个有序链表需要分治+归并,对K个链表进行分治,对分治后的链表两两归并排序即可。
public class _7合并K个排序链表 {
public static class ListNode {
int val;
ListNode next;
ListNode() {}
ListNode(int val) { this.val = val; }
ListNode(int val, ListNode next) { this.val = val; this.next = next; }
}
public static ListNode mergeTwoLists(ListNode l1, ListNode l2) {
ListNode temp1=l1;
ListNode temp2=l2;
ListNode ret=new ListNode(0);
ListNode ret1=ret;
while (temp1!=null&&temp2!=null){
if (temp1.val>temp2.val){
ret.next=temp2;
temp2=temp2.next;
}else {
ret.next=temp1;
temp1=temp1.next;
}
ret=ret.next;
}
if (temp1!=null&&temp2==null){
ret.next=temp1;
}
if (temp2!=null&&temp1==null){
ret.next=temp2;
}
return ret1.next;
}
public ListNode mergeSolution(ListNode[] lists, int begin, int end) {
if(begin == end) {
return lists[begin];
}else {
int mid = (begin + end) >> 1;
ListNode l1 = mergeSolution(lists, begin, mid);
ListNode l2 = mergeSolution(lists, mid + 1, end);
return mergeTwoLists(l1, l2);
}
}
public ListNode mergeKLists(ListNode[] lists) {
if(lists == null || lists.length == 0) return null;
return mergeSolution(lists, 0, lists.length - 1);
}
}
8、二叉树的最近公共祖先
给定一个二叉树, 找到该树中两个指定节点的最近公共祖先。
百度百科中最近公共祖先的定义为:“对于有根树 T 的两个结点 p、q,最近公共祖先表示为一个结点 x,满足 x 是 p、q 的祖先且 x 的深度尽可能大(一个节点也可以是它自己的祖先)。”
例如,给定如下二叉树: root = [3,5,1,6,2,0,8,null,null,7,4]
示例 1:
输入: root = [3,5,1,6,2,0,8,null,null,7,4], p = 5, q = 1
输出: 3
解释: 节点 5 和节点 1 的最近公共祖先是节点 3。
示例 2:
输入: root = [3,5,1,6,2,0,8,null,null,7,4], p = 5, q = 4
输出: 5
解释: 节点 5 和节点 4 的最近公共祖先是节点 5。因为根据定义最近公共祖先节点可以为节点本身。
说明:
所有节点的值都是唯一的。
p、q 为不同节点且均存在于给定的二叉树中。
package LeetCode.byte_beating.链表与树;
//https://leetcode-cn.com/problems/lowest-common-ancestor-of-a-binary-tree/solution/er-cha-shu-de-zui-jin-gong-gong-zu-xian-by-leetc-2/
public class _8二叉树的最近公共祖先 {
private TreeNode ans;
public class TreeNode {
int val;
TreeNode left;
TreeNode right;
TreeNode(int x) { val = x; }
}
private boolean dfs(TreeNode root, TreeNode p, TreeNode q) {
if (root == null) return false;
boolean lson = dfs(root.left, p, q);
boolean rson = dfs(root.right, p, q);
if ((lson && rson) || ((root.val == p.val || root.val == q.val) && (lson || rson))) {
ans = root;
}
return lson || rson || (root.val == p.val || root.val == q.val);
}
public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
this.dfs(root, p, q);
return this.ans;
}
}
9、二叉树的锯齿形层次遍历
给定一个二叉树,返回其节点值的锯齿形层次遍历。(即先从左往右,再从右往左进行下一层遍历,以此类推,层与层之间交替进行)。
例如:
给定二叉树 [3,9,20,null,null,15,7],
3
/
9 20
/
15 7
返回锯齿形层次遍历如下:
[
[3],
[20,9],
[15,7]
]
package LeetCode.byte_beating.链表与树;
import java.util.*;
//层序遍历思路,偶数列反转
public class _9二叉树的锯齿形层次遍历 {
public class TreeNode {
int val;
TreeNode left;
TreeNode right;
TreeNode(int x) { val = x; }
}
public List<List<Integer>> zigzagLevelOrder(TreeNode root) {
if(root == null){
return new ArrayList<>();
}
List<List<Integer>> res = new ArrayList<>();
Queue<TreeNode> list = new LinkedList<>();
list.offer(root);
boolean normalOrder = true;
while(!list.isEmpty()){
int size = list.size();
ArrayList<Integer> level = new ArrayList<>();
for(int i = 0; i < size; i++){
TreeNode cur = list.poll();
level.add(cur.val);
if(cur.left != null){
list.offer(cur.left);
}
if(cur.right != null){
list.offer(cur.right);
}
}
if (normalOrder) {
res.add(level);
}else {
Collections.reverse(level);
res.add(level);
}
normalOrder = !normalOrder;
}
return res;
}
}
四、动态或贪心
1、买卖股票的最佳时机
给定一个数组,它的第 i 个元素是一支给定股票第 i 天的价格。
如果你最多只允许完成一笔交易(即买入和卖出一支股票一次),设计一个算法来计算你所能获取的最大利润。
注意:你不能在买入股票前卖出股票。
示例 1:
输入: [7,1,5,3,6,4]
输出: 5
解释: 在第 2 天(股票价格 = 1)的时候买入,在第 5 天(股票价格 = 6)的时候卖出,最大利润 = 6-1 = 5 。
注意利润不能是 7-1 = 6, 因为卖出价格需要大于买入价格;同时,你不能在买入前卖出股票。
示例 2:
输入: [7,6,4,3,1]
输出: 0
解释: 在这种情况下, 没有交易完成, 所以最大利润为 0。
package LeetCode.byte_beating.动态或贪心;
//使我们感兴趣的点是上图中的峰和谷。我们需要找到当前为止最小的谷(不一定是全局最小)和之后的最大的峰。
// 我们可以维持两个变量——minprice 和 maxprofit,它们分别对应迄今为止所得到的最小的谷值和最大的利润(卖出价格与最低价格之间的最大差值)。
public class _1买卖股票的最佳时机 {
public static int maxProfit(int prices[]) {
int minprice = Integer.MAX_VALUE;
int maxprofit = 0;
for (int i = 0; i < prices.length; i++) {
if (prices[i] < minprice)
minprice = prices[i];
else if (prices[i] - minprice > maxprofit)
maxprofit = prices[i] - minprice;
}
return maxprofit;
}
public static void main(String[] args) {
int[] a= new int[]{7,1,5,6,4};
System.out.println(maxProfit(a));
}
}
2、买卖股票的最佳时机2
给定一个数组,它的第 i 个元素是一支给定股票第 i 天的价格。
设计一个算法来计算你所能获取的最大利润。你可以尽可能地完成更多的交易(多次买卖一支股票)。
注意:你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。
示例 1:
输入: [7,1,5,3,6,4]
输出: 7
解释: 在第 2 天(股票价格 = 1)的时候买入,在第 3 天(股票价格 = 5)的时候卖出, 这笔交易所能获得利润 = 5-1 = 4 。
随后,在第 4 天(股票价格 = 3)的时候买入,在第 5 天(股票价格 = 6)的时候卖出, 这笔交易所能获得利润 = 6-3 = 3 。
示例 2:
输入: [1,2,3,4,5]
输出: 4
解释: 在第 1 天(股票价格 = 1)的时候买入,在第 5 天 (股票价格 = 5)的时候卖出, 这笔交易所能获得利润 = 5-1 = 4 。
注意你不能在第 1 天和第 2 天接连购买股票,之后再将它们卖出。
因为这样属于同时参与了多笔交易,你必须在再次购买前出售掉之前的股票。
示例 3:
输入: [7,6,4,3,1]
输出: 0
解释: 在这种情况下, 没有交易完成, 所以最大利润为 0。
提示:
1 <= prices.length <= 3 * 10 ^ 4
0 <= prices[i] <= 10 ^ 4
package LeetCode.byte_beating.动态或贪心;
public class _2买卖股票的最佳时机2 {
public static int maxProfit(int[] prices) {
int i = 0;
int valley = prices[0];
int peak = prices[0];
int maxprofit = 0;
while (i < prices.length - 1) {
while (i < prices.length - 1 && prices[i] >= prices[i + 1])
i++;
valley = prices[i];
while (i < prices.length - 1 && prices[i] <= prices[i + 1])
i++;
peak = prices[i];
maxprofit += peak - valley;
}
return maxprofit;
}
public static void main(String[] args) {
int[] a= new int[]{7,1,5,3,6,4};
System.out.println(maxProfit(a));
}
}
3、最大正方形
在一个由 0 和 1 组成的二维矩阵内,找到只包含 1 的最大正方形,并返回其面积。
示例:
输入:
1 0 1 0 0
1 0 1 1 1
1 1 1 1 1
1 0 0 1 0
输出: 4
package LeetCode.byte_beating.动态或贪心;
//https://leetcode-cn.com/problems/maximal-square/solution/zui-da-zheng-fang-xing-by-leetcode-solution/
public class _3最大正方形 {
//由于正方形的面积等于边长的平方,因此要找到最大正方形的面积,首先需要找到最大正方形的边长,然后计算最大边长的平方即可。
//
//暴力法是最简单直观的做法,具体做法如下:
//
//遍历矩阵中的每个元素,每次遇到 1,则将该元素作为正方形的左上角;
//
//确定正方形的左上角后,根据左上角所在的行和列计算可能的最大正方形的边长(正方形的范围不能超出矩阵的行数和列数),在该边长范围内寻找只包含 1 的最大正方形;
//
//每次在下方新增一行以及在右方新增一列,判断新增的行和列是否满足所有元素都是 1
//复杂度分析
//
//时间复杂度:O(mn \min(m,n)^2)O(mnmin(m,n)2),其中 mm 和 nn 是矩阵的行数和列数。
//
//需要遍历整个矩阵寻找每个 11,遍历矩阵的时间复杂度是 O(mn)O(mn)。
//对于每个可能的正方形,其边长不超过 mm 和 nn 中的最小值,需要遍历该正方形中的每个元素判断是不是只包含 11,遍历正方形时间复杂度是 O(\min(m,n)^2)O(min(m,n)2)。
//总时间复杂度是 O(mn \min(m,n)^2)O(mnmin(m,n)2)。
//空间复杂度:O(1)O(1)。额外使用的空间复杂度为常数。
// public int maximalSquare(char[][] matrix) {
// int maxSide = 0;
// if (matrix == null || matrix.length == 0 || matrix[0].length == 0) {
// return maxSide;
// }
// int rows = matrix.length, columns = matrix[0].length;
// for (int i = 0; i < rows; i++) {
// for (int j = 0; j < columns; j++) {
// if (matrix[i][j] == '1') {
// // 遇到一个 1 作为正方形的左上角
// maxSide = Math.max(maxSide, 1);
// // 计算可能的最大正方形边长
// int currentMaxSide = Math.min(rows - i, columns - j);
// for (int k = 1; k < currentMaxSide; k++) {
// // 判断新增的一行一列是否均为 1
// boolean flag = true;
// if (matrix[i + k][j + k] == '0') {
// break;
// }
// for (int m = 0; m < k; m++) {
// if (matrix[i + k][j + m] == '0' || matrix[i + m][j + k] == '0') {
// flag = false;
// break;
// }
// }
// if (flag) {
// maxSide = Math.max(maxSide, k + 1);
// } else {
// break;
// }
// }
// }
// }
// }
// int maxSquare = maxSide * maxSide;
// return maxSquare;
// }
//可以使用动态规划降低时间复杂度。我们用 dp(i, j)dp(i,j) 表示以 (i, j)(i,j) 为右下角,且只包含 11 的正方形的边长最大值。
// 如果我们能计算出所有 dp(i, j)dp(i,j) 的值,那么其中的最大值即为矩阵中只包含 11 的正方形的边长最大值,其平方即为最大正方形的面积。
//那么如何计算 dpdp 中的每个元素值呢?对于每个位置 (i, j)(i,j),检查在矩阵中该位置的值:
//如果该位置的值是 00,则 dp(i, j) = 0dp(i,j)=0,因为当前位置不可能在由 11 组成的正方形中;
//如果该位置的值是 11,则 dp(i, j)dp(i,j) 的值由其上方、左方和左上方的三个相邻位置的 dpdp 值决定。
// 具体而言,当前位置的元素值等于三个相邻位置的元素中的最小值加 11,状态转移方程如下:
//dp(i, j)=min(dp(i−1, j), dp(i−1, j−1), dp(i, j−1))+1
//如果读者对这个状态转移方程感到不解,可以参考 1277. 统计全为 1 的正方形子矩阵的官方题解,其中给出了详细的证明。
//
//此外,还需要考虑边界条件。如果 ii 和 jj 中至少有一个为 00,则以位置 (i, j)(i,j) 为右下角的最大正方形的边长只能是 11,因此 dp(i, j) = 1dp(i,j)=1。
public int maximalSquare(char[][] matrix) {
int maxSide = 0;
if (matrix == null || matrix.length == 0 || matrix[0].length == 0) {
return maxSide;
}
int rows = matrix.length, columns = matrix[0].length;
int[][] dp = new int[rows][columns];
for (int i = 0; i < rows; i++) {
for (int j = 0; j < columns; j++) {
if (matrix[i][j] == '1') {
if (i == 0 || j == 0) {
dp[i][j] = 1;
} else {
dp[i][j] = Math.min(Math.min(dp[i - 1][j], dp[i][j - 1]), dp[i - 1][j - 1]) + 1;
}
maxSide = Math.max(maxSide, dp[i][j]);
}
}
}
int maxSquare = maxSide * maxSide;
return maxSquare;
}
}
4、最大子序和
给定一个整数数组 nums ,找到一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。
示例:
输入: [-2,1,-3,4,-1,2,1,-5,4],
输出: 6
解释: 连续子数组 [4,-1,2,1] 的和最大,为 6。
进阶:
如果你已经实现复杂度为 O(n) 的解法,尝试使用更为精妙的分治法求解。
package LeetCode.byte_beating.动态或贪心;
public class _4最大子序和 {
//这道题用动态规划的思路并不难解决,比较难的是后文提出的用分治法求解,但由于其不是最优解法,所以先不列出来
//动态规划的是首先对数组进行遍历,当前最大连续子序列和为 sum,结果为 ans
//如果 sum > 0,则说明 sum 对结果有增益效果,则 sum 保留并加上当前遍历数字
//如果 sum <= 0,则说明 sum 对结果无增益效果,需要舍弃,则 sum 直接更新为当前遍历数字
//每次比较 sum 和 ans的大小,将最大值置为ans,遍历结束返回结果
//时间复杂度:O(n)O(n)
public static int maxSubArray(int[] array) {
int sum =0;
int max = array[0];
for(int i=0;i<array.length;i++){
if(sum>=0)
{ //首先进行求和
sum= sum+array[i];
}else{
//求和的正数区间结束,将当前的值设置为初值
sum=array[i];
}
if(sum>max)
//记录每个求和区间的最大值
max = sum;
}
return max;
}
public static void main(String[] args) {
int[] a=new int [] {-2,1,-3,4,-1,2,1,-5,4};
System.out.println(maxSubArray(a));
}
}
5、三角形最小路径和
给定一个三角形,找出自顶向下的最小路径和。每一步只能移动到下一行中相邻的结点上。
相邻的结点 在这里指的是 下标 与 上一层结点下标 相同或者等于 上一层结点下标 + 1 的两个结点。
例如,给定三角形:
[
[2],
[3,4],
[6,5,7],
[4,1,8,3]
]
自顶向下的最小路径和为 11(即,2 + 3 + 5 + 1 = 11)。
说明:
如果你可以只使用 O(n) 的额外空间(n 为三角形的总行数)来解决这个问题,那么你的算法会很加分。
package LeetCode.byte_beating.动态或贪心;
import java.util.List;
//dp代表遍历该层下标为i的从小到此元素的最小路径
//一层一层向上遍历 dp[i]覆盖下一层的dp[i] 最上一层的dp[0] 就是我们所的三角形最小路
public class _5三角形最小路径和 {
public int minimumTotal(List<List<Integer>> triangle) {
int n=triangle.size();
int []dp=new int[n];
for(int i=n-1;i>=0;i--){
for(int j=0;j<triangle.get(i).size();j++){
if(i==n-1){
dp[j]=triangle.get(i).get(j); //初始化最后一层dp
}else{
dp[j]=Math.min(dp[j]+triangle.get(i).get(j),dp[j+1]+triangle.get(i).get(j));
}
}
}
return dp[0];
}
}
6、俄罗斯套娃信封问题
给定一些标记了宽度和高度的信封,宽度和高度以整数对形式 (w, h) 出现。当另一个信封的宽度和高度都比这个信封大的时候,这个信封就可以放进另一个信封里,如同俄罗斯套娃一样。
请计算最多能有多少个信封能组成一组“俄罗斯套娃”信封(即可以把一个信封放到另一个信封里面)。
说明:
不允许旋转信封。
示例:
输入: envelopes = [[5,4],[6,4],[6,7],[2,3]]
输出: 3
解释: 最多信封的个数为 3, 组合为: [2,3] => [5,4] => [6,7]。
package LeetCode.byte_beating.动态或贪心;
import java.util.Arrays;
//https://www.cnblogs.com/xym4869/p/12712758.html
//这道题⽬其实是最⻓递增⼦序列(Longes Increasing Subsequence,简写为LIS)的⼀个变种,因为很显然,
// 每次合法的嵌套是⼤的套⼩的,相当于找⼀个最⻓递增的⼦序列,其⻓度就是最多能嵌套的信封个数。但是难点在于,
// 标准的 LIS 算法只能在数组中寻找最⻓⼦序列,⽽我们的信封是由 (w, h) 这样的⼆维数对形式表⽰的,如何把 LIS 算法运⽤过来呢?
//这道题的解法是⽐较巧妙的:先对宽度 w 进⾏升序排序,如果遇到 w 相同的情况,则按照⾼度 h 降序排序。
// 之后把所有的 h 作为⼀个数组,在这个数组上计算 LIS 的⻓度就是答案。
public class _6俄罗斯套娃信封问题 {
public int maxEnvelopes(int[][] envelopes) {
if (envelopes.length == 0)
return 0;
// 按宽度升序排列,如果宽度⼀样,则按⾼度降序排列
Arrays.sort(envelopes, (t1, t2) -> {
if (t1[0] < t2[0])
return -1;
else if (t1[0] == t2[0]) {
if (t1[1] > t2[1])
return -1;
else if (t1[1] == t2[1])
return 0;
}
return 1;
});
// 对⾼度数组寻找 LIS
int[] dp = new int[envelopes.length];
Arrays.fill(dp, 1);
int max = 1;
for (int i = 1; i < envelopes.length; i++) {
for (int j = 0; j < i; j++) {
if (envelopes[i][1] > envelopes[j][1])
dp[i] = Math.max(dp[i], dp[j] + 1);
}
max = Math.max(max, dp[i]);
}
return max;
}
}
7、最长上升子序列
package LeetCode.byte_beating.动态或贪心;
//https://leetcode-cn.com/problems/longest-increasing-subsequence/solution/zui-chang-shang-sheng-zi-xu-lie-by-leetcode-soluti/
public class _7最长上升子序列 {
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 < dp.length; i++) {
int maxval = 0;
for (int j = 0; j < i; j++) {
if (nums[i] > nums[j]) {
maxval = Math.max(maxval, dp[j]);
}
}
dp[i] = maxval + 1;
maxans = Math.max(maxans, dp[i]);
}
return maxans;
}
}
package LeetCode.byte_beating.动态或贪心;
//https://leetcode-cn.com/problems/longest-increasing-subsequence/solution/zui-chang-shang-sheng-zi-xu-lie-by-leetcode-soluti/
public class _7最长上升子序列 {
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 < dp.length; i++) {
int maxval = 0;
for (int j = 0; j < i; j++) {
if (nums[i] > nums[j]) {
maxval = Math.max(maxval, dp[j]);
}
}
dp[i] = maxval + 1;
maxans = Math.max(maxans, dp[i]);
}
return maxans;
}
}
五、数据结构
1、最小栈
设计一个支持 push ,pop ,top 操作,并能在常数时间内检索到最小元素的栈。
push(x) —— 将元素 x 推入栈中。
pop() —— 删除栈顶的元素。
top() —— 获取栈顶元素。
getMin() —— 检索栈中的最小元素。
示例:
输入:
[“MinStack”,“push”,“push”,“push”,“getMin”,“pop”,“top”,“getMin”]
[[],[-2],[0],[-3],[],[],[],[]]
输出:
[null,null,null,null,-3,null,0,-2]
解释:
MinStack minStack = new MinStack();
minStack.push(-2);
minStack.push(0);
minStack.push(-3);
minStack.getMin(); --> 返回 -3.
minStack.pop();
minStack.top(); --> 返回 0.
minStack.getMin(); --> 返回 -2.
提示:
pop、top 和 getMin 操作总是在 非空栈 上调用。
https://leetcode-cn.com/problems/min-stack/solution/zui-xiao-zhan-by-leetcode-solution/
//解题思路:
//借用一个辅助栈min_stack,用于存获取stack中最小值。
//算法流程:
//push()方法: 每当push()新值进来时,如果 小于等于 min_stack栈顶值,则一起push()到min_stack,即更新了栈顶最小值;
//pop()方法: 判断将pop()出去的元素值是否是min_stack栈顶元素值(即最小值),如果是则将min_stack栈顶元素一起pop(),
// 这样可以保证min_stack栈顶元素始终是stack中的最小值。
//getMin()方法: 返回min_stack栈顶即可。
//min_stack作用分析:
//min_stack等价于遍历stack所有元素,把升序的数字都删除掉,留下一个从栈底到栈顶降序的栈。
//相当于给stack中的降序元素做了标记,每当pop()这些降序元素,min_stack会将相应的栈顶元素pop()出去,保证其栈顶元素始终是stack中的最小元素。
//复杂度分析:
//时间复杂度 O(1) :压栈,出栈,获取最小值的时间复杂度都为 O(1) 。
//空间复杂度 O(N) :包含 N个元素辅助栈占用线性大小的额外空间。
import java.util.Stack;
public class _1最小栈 {
private Stack<Integer> stack;
private Stack<Integer> min_stack;
public _1最小栈() {
stack = new Stack<>();
min_stack = new Stack<>();
}
public void push(int x) {
stack.push(x);
if(min_stack.isEmpty() || x <= min_stack.peek())
min_stack.push(x);
}
public void pop() {
if(stack.pop().equals(min_stack.peek()))
min_stack.pop();
}
public int top() {
return stack.peek();
}
public int getMin() {
return min_stack.peek();
}
}
2、 LRU缓存机制
运用你所掌握的数据结构,设计和实现一个 LRU (最近最少使用) 缓存机制。它应该支持以下操作: 获取数据 get 和 写入数据 put 。
获取数据 get(key) - 如果关键字 (key) 存在于缓存中,则获取关键字的值(总是正数),否则返回 -1。
写入数据 put(key, value) - 如果关键字已经存在,则变更其数据值;如果关键字不存在,则插入该组「关键字/值」。当缓存容量达到上限时,它应该在写入新数据之前删除最久未使用的数据值,从而为新的数据值留出空间。
进阶:
你是否可以在 O(1) 时间复杂度内完成这两种操作?
https://leetcode-cn.com/problems/lru-cache/solution/lruhuan-cun-ji-zhi-by-leetcode-solution/
//算法
//
//LRU 缓存机制可以通过哈希表辅以双向链表实现,我们用一个哈希表和一个双向链表维护所有在缓存中的键值对。
//
//双向链表按照被使用的顺序存储了这些键值对,靠近头部的键值对是最近使用的,而靠近尾部的键值对是最久未使用的。
//
//哈希表即为普通的哈希映射(HashMap),通过缓存数据的键映射到其在双向链表中的位置。
//
//这样以来,我们首先使用哈希表进行定位,找出缓存项在双向链表中的位置,随后将其移动到双向链表的头部,即可在 O(1) 的时间内完成 get 或者 put 操作。具体的方法如下:
//
//对于 get 操作,首先判断 key 是否存在:
//
//如果 key 不存在,则返回 -1;
//
//如果 key 存在,则 key 对应的节点是最近被使用的节点。通过哈希表定位到该节点在双向链表中的位置,并将其移动到双向链表的头部,最后返回该节点的值。
//
//对于 put 操作,首先判断 key 是否存在:
//
//如果 key 不存在,使用 key 和 value 创建一个新的节点,在双向链表的头部添加该节点,并将 key 和该节点添加进哈希表中。
// 然后判断双向链表的节点数是否超出容量,如果超出容量,则删除双向链表的尾部节点,并删除哈希表中对应的项;
//
//如果 key 存在,则与 get 操作类似,先通过哈希表定位,再将对应的节点的值更新为 value,并将该节点移到双向链表的头部。
//
//上述各项操作中,访问哈希表的时间复杂度为 O(1),在双向链表的头部添加节点、在双向链表的尾部删除节点的复杂度也为 O(1)。
// 而将一个节点移到双向链表的头部,可以分成「删除该节点」和「在双向链表的头部添加节点」两步操作,都可以在 O(1) 时间内完成。
//小贴士
//在双向链表的实现中,使用一个伪头部(dummy head)和伪尾部(dummy tail)标记界限,这样在添加节点和删除节点的时候就不需要检查相邻的节点是否存在。
public class _2LRU缓存机制 {
class DLinkedNode {
int key;
int value;
DLinkedNode prev;
DLinkedNode next;
public DLinkedNode() {}
public DLinkedNode(int _key, int _value) {key = _key; value = _value;}
}
private Map<Integer, DLinkedNode> cache = new HashMap<Integer, DLinkedNode>();
private int size;
private int capacity;
private DLinkedNode head, tail;
public _2LRU缓存机制(int capacity) {
this.size = 0;
this.capacity = capacity;
// 使用伪头部和伪尾部节点
head = new DLinkedNode();
tail = new DLinkedNode();
head.next = tail;
tail.prev = head;
}
public int get(int key) {
DLinkedNode node = cache.get(key);
if (node == null) {
return -1;
}
// 如果 key 存在,先通过哈希表定位,再移到头部
moveToHead(node);
return node.value;
}
public void put(int key, int value) {
DLinkedNode node = cache.get(key);
if (node == null) {
// 如果 key 不存在,创建一个新的节点
DLinkedNode newNode = new DLinkedNode(key, value);
// 添加进哈希表
cache.put(key, newNode);
// 添加至双向链表的头部
addToHead(newNode);
++size;
if (size > capacity) {
// 如果超出容量,删除双向链表的尾部节点
DLinkedNode tail = removeTail();
// 删除哈希表中对应的项
cache.remove(tail.key);
--size;
}
}
else {
// 如果 key 存在,先通过哈希表定位,再修改 value,并移到头部
node.value = value;
moveToHead(node);
}
}
private void addToHead(DLinkedNode node) {
node.prev = head;
node.next = head.next;
head.next.prev = node;
head.next = node;
}
private void removeNode(DLinkedNode node) {
node.prev.next = node.next;
node.next.prev = node.prev;
}
private void moveToHead(DLinkedNode node) {
removeNode(node);
addToHead(node);
}
private DLinkedNode removeTail() {
DLinkedNode res = tail.prev;
removeNode(res);
return res;
}
}
3、全 O(1) 的数据结构
请你实现一个数据结构支持以下操作:
Inc(key) - 插入一个新的值为 1 的 key。或者使一个存在的 key 增加一,保证 key 不为空字符串。
Dec(key) - 如果这个 key 的值是 1,那么把他从数据结构中移除掉。否则使一个存在的 key 值减一。如果这个 key 不存在,这个函数不做任何事情。key 保证不为空字符串。
GetMaxKey() - 返回 key 中值最大的任意一个。如果没有元素存在,返回一个空字符串"" 。
GetMinKey() - 返回 key 中值最小的任意一个。如果没有元素存在,返回一个空字符串""。
挑战:
你能够以 O(1) 的时间复杂度实现所有操作吗?
https://leetcode-cn.com/problems/all-oone-data-structure/solution/li-yong-java-mapjie-kou-zhong-de-mergehe-computefa/
//整体思路是跟大家一样的,利用hashmap和同时维护双向链表实现查找和获取最大值、最小值的key,所以不再赘述思路。
// 在inc和dec函数需要判断key是否存在和key的value是否是1,然后再去做相应的操作,代码会有点多,利用java核心技术卷1(第11版)
// 中9.4.2中介绍的Map接口的merge和compute函数可以使代码更简洁。
public class _3全常数的数据结构 {
class AllOne {
private Node head;
private Node tail;
private Map<String, Node> map;
/** Initialize your data structure here. */
public AllOne() {
head = new Node("head", -1);
tail = new Node("tail", -1);
head.next = tail;
tail.prev = head;
map = new HashMap<>();
}
/** Inserts a new key <Key> with value 1. Or increments an existing key by 1. */
public void inc(String key) {
// merge函数会判断map中是否存在key,如果不存在就会用merge函数的第二个参数作为key的value存入map
// 如果key已经存在,就会利用merge函数的第三个参数,这个参数接收一个函数式接口
// 该函数式接口会对已经存在的key的value和你提供的value进行操作(就是merge函数的第二个参数)
// 由于本题只需要对已经存在的key的value加一,故不需要操作你提供的value
// merge函数会将已经存在的key和函数式接口返回的value关联起来
// 由于本题要求,故在函数式接口中直接返回原来的value即可
map.merge(key, new Node(key, 1), (node1, node2) -> {
node1.value++;
return node1;
});
map.get(key).moveToTail(head, tail);
}
/** Decrements an existing key by 1. If Key's value is 1, remove it from the data structure. */
public void dec(String key) {
Node temp = map.get(key);
// computeIfPresent函数其实本质上与merge函数基本相同
// computeIfPresent函数与merge函数的区别在于computeIfPresent函数不会提供默认值
// 也就意味着如果你提供的key不在map中computeIfPresent函数什么也不做
// 相反如果key存在就会执行你提供的函数式接口里面的操作(computeIfPresent函数的第二个参数)
// 如果你在函数式接口里面返回了null,那computeIfPresent函数会将该key删除(当前前提是该key存在)
map.computeIfPresent(key, (k, node) -> {
node.value--;
if (node.value == 0) {
return null;
}
return node;
});
if (map.containsKey(key)) {
map.get(key).moveToHead(head);
} else {
if (temp != null) {
temp.delete();
}
}
}
/** Returns one of the keys with maximal value. */
public String getMaxKey() {
if (map.isEmpty()) {
return "";
} else {
return tail.prev.key;
}
}
/** Returns one of the keys with Minimal value. */
public String getMinKey() {
if (map.isEmpty()) {
return "";
} else {
return head.next.key;
}
}
}
class Node {
String key;
int value;
Node prev;
Node next;
public Node(String key, int value) {
this.key = key;
this.value = value;
prev = null;
next = null;
}
public void moveToTail(Node head, Node tail) {
if (prev == next && prev == null) {
next = head.next;
prev = head;
next.prev = this;
head.next = this;
} else if (next != tail) {
while (value > next.value && next != tail) {
prev.next = next;
next.prev = prev;
Node temp = next;
next = temp.next;
temp.next = this;
prev = temp;
next.prev = this;
}
}
}
public void moveToHead(Node head) {
if (prev != head) {
if (value < prev.value) {
prev.next = next;
next.prev = prev;
Node temp = prev;
next = temp;
prev = temp.prev;
prev.next = this;
temp.prev = this;
}
}
}
public void delete() {
prev.next = next;
next.prev = prev;
prev = null;
next = null;
}
}
}
六、扩展练习
1、实现 int sqrt(int x) 函数。
计算并返回 x 的平方根,其中 x 是非负整数。
由于返回类型是整数,结果只保留整数的部分,小数部分将被舍去。
示例 1:
输入: 4
输出: 2
示例 2:
输入: 8
输出: 2
说明: 8 的平方根是 2.82842…,
由于返回类型是整数,小数部分将被舍去。
https://leetcode-cn.com/problems/sqrtx/solution/x-de-ping-fang-gen-by-leetcode-solution/
//「袖珍计算器算法」是一种用指数函数 exp 和对数函数ln 代替平方根函数的方法。我们通过有限的可以使用的数学函数,得到我们想要计算的结果。
//注意: 由于计算机无法存储浮点数的精确值(浮点数的存储方法可以参考 IEEE 754,这里不再赘述),而指数函数和对数函数的参数和返回值均为浮点数,
// 因此运算过程中会存在误差。例如当 x = 2147395600x=2147395600 时,的计算结果与正确值 4634046340 相差 10^{-11}
//这样在对结果取整数部分时,会得到 4633946339 这个错误的结果。
//因此在得到结果的整数部分 \textit{ans}ans 后,我们应当找出 ans 与 ans+1 中哪一个是真正的答案。
//https://leetcode-cn.com/problems/sqrtx/solution/x-de-ping-fang-gen-by-leetcode-solution/
public class _1x的平方根 {
public int mySqrt(int x) {
if (x == 0) {
return 0;
}
int ans = (int)Math.exp(0.5 * Math.log(x));
return (long)(ans + 1) * (ans + 1) <= x ? ans + 1 : ans;
}
}
2、UTF-8 编码验证
UTF-8 中的一个字符可能的长度为 1 到 4 字节,遵循以下的规则:
对于 1 字节的字符,字节的第一位设为0,后面7位为这个符号的unicode码。
对于 n 字节的字符 (n > 1),第一个字节的前 n 位都设为1,第 n+1 位设为0,后面字节的前两位一律设为10。剩下的没有提及的二进制位,全部为这个符号的unicode码。
这是 UTF-8 编码的工作方式:
Char. number range | UTF-8 octet sequence
(hexadecimal) | (binary)
--------------------±--------------------------------------------
0000 0000-0000 007F | 0xxxxxxx
0000 0080-0000 07FF | 110xxxxx 10xxxxxx
0000 0800-0000 FFFF | 1110xxxx 10xxxxxx 10xxxxxx
0001 0000-0010 FFFF | 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx
给定一个表示数据的整数数组,返回它是否为有效的 utf-8 编码。
注意:
输入是整数数组。只有每个整数的最低 8 个有效位用来存储数据。这意味着每个整数只表示 1 字节的数据。
示例 1:
data = [197, 130, 1], 表示 8 位的序列: 11000101 10000010 00000001.
返回 true 。
这是有效的 utf-8 编码,为一个2字节字符,跟着一个1字节字符。
示例 2:
data = [235, 140, 4], 表示 8 位的序列: 11101011 10001100 00000100.
返回 false 。
前 3 位都是 1 ,第 4 位为 0 表示它是一个3字节字符。
下一个字节是开头为 10 的延续字节,这是正确的。
但第二个延续字节不以 10 开头,所以是不符合规则的。
https://leetcode-cn.com/problems/utf-8-validation/solution/utf-8-bian-ma-yan-zheng-by-leetcode/
//对数组中每个整数进行处理。
//对于每个整数,获取当前整数 字符串形式 的二进制表示。考虑到整数可能非常大,我们只需要只保留 最高 8 比特 就可以了。完成这个步骤之后,
// 我们会获得一个 8 比特的二进制字符串数组。我们把这个数组叫做 bin_rep。
//在接下来的步骤中有两个情况需要处理。
//第一个情况是,我们正在处理某个 UTF-8 字符之中。在这个情况下,我们只需要检查前两个比特是不是 10 就可以了。bin_rep[:2] == "10"
//第二个情况是刚开始处理一个新的 UTF-8字符。在这个情况下,我们需要查看这个字节的前缀,计算第一个 0 之前 1 的个数。这会告诉我们当前 UTF-8 字符的大小。
//对于数组中的每个整数都这样处理,直到数组为空或者发现非法字符。
//https://leetcode-cn.com/problems/utf-8-validation/solution/utf-8-bian-ma-yan-zheng-by-leetcode/
public class _2UTF8编码验证 {
class Solution {
public boolean validUtf8(int[] data) {
// 当前UTF-8字符的字节数
int numberOfBytesToProcess = 0;
// For each 数据数组中的每个整数
for (int i = 0; i < data.length; i++) {
// 获取二进制表示。我们只需要最高的8位
// for any given number.
String binRep = Integer.toBinaryString(data[i]);
binRep =
binRep.length() >= 8
? binRep.substring(binRep.length() - 8)
: "00000000".substring(binRep.length() % 8) + binRep;
// 如果是这种情况,那么我们将开始处理一个新的UTF-8字符。
if (numberOfBytesToProcess == 0) {
// 获取字符串开头的1的数量。
for (int j = 0; j < binRep.length(); j++) {
if (binRep.charAt(j) == '0') {
break;
}
numberOfBytesToProcess += 1;
}
// 1个字节字符
if (numberOfBytesToProcess == 0) {
continue;
}
// 根据问题的规则无效的场景。.
if (numberOfBytesToProcess > 4 || numberOfBytesToProcess == 1) {
return false;
}
} else {
// 我们处理的是表示字节的整数,字节是
//一个utf - 8字符, 他们必须遵守模式`10xxxxxx`.
if (!(binRep.charAt(0) == '1' && binRep.charAt(1) == '0')) {
return false;
}
}
//我们在每个整数之后将处理的字节数减少1
numberOfBytesToProcess -= 1;
}
// 这是针对我们可能没有完整数据的情况
//一个特定的UTF-8字符。
return numberOfBytesToProcess == 0;
}
}
}
3、 第二高的薪水
编写一个 SQL 查询,获取 Employee 表中第二高的薪水(Salary) 。
±—±-------+
| Id | Salary |
±—±-------+
| 1 | 100 |
| 2 | 200 |
| 3 | 300 |
±—±-------+
例如上述 Employee 表,SQL查询应该返回 200 作为第二高的薪水。如果不存在第二高的薪水,那么查询应返回 null。
±--------------------+
| SecondHighestSalary |
±--------------------+
| 200 |
±--------------------+
https://leetcode-cn.com/problems/second-highest-salary/solution/di-er-gao-de-xin-shui-by-leetcode/
public class _3第二高薪水 {
//SELECT
// (SELECT DISTINCT
// Salary
// FROM
// Employee
// ORDER BY Salary DESC
// LIMIT 1 OFFSET 1) AS SecondHighestSalary
//;
}