概述
给定一个包含 n 个整数的数组 nums,判断 nums 中是否存在三个元素 a,b,c ,使得 a + b + c = 0 ?找出所有满足条件且不重复的三元组。
注意:答案中不可以包含重复的三元组。
例如, 给定数组 nums = [-1, 0, 1, 2, -1, -4],
满足要求的三元组集合为:
[
[-1, 0, 1],
[-1, -1, 2]
]
这个题目看似很简单,但是一旦考虑起来时间复杂度,就不简单了,最开始我使用暴力破解法,的确能得到正确的结果,但是一直得不到leetcode测试程序的认可,反馈是耗时过长。增加时间打印发现是循环的复杂度太高。最开始的代码如下:
主要思路如下:
定义三个指针,去依次遍历。
第一次循环固定前面两个,后面第三个去滑动。每次判断指针指向的三个数字,判断非空就加进去。这里有个小优化就是判断是否存在采用了HashSet自动去重。但是从结果看还是消耗太高。更优的做法是先不去重,所有循环结束再去重耗时更小,有点类似于以空间换时间的感觉。
public List<List<Integer>> threeSum(int[] nums) {
Set<List<Integer>> set = new HashSet<>();
if (nums.length < 3) {
return new ArrayList<>();
}
// 排序
for (int i = 0; i < nums.length; i++) {
for (int j = 0; j < nums.length - 1 - i; j++) {
if (nums[j] > nums[j + 1]) {
int temp = nums[j];
nums[j] = nums[j + 1];
nums[j + 1] = temp;
}
}
}
List<Integer> tempList = new ArrayList<>();
for (int i = 0; i < nums.length; i++) {
if (nums[i] > 0) {
break;
}
for (int j = i + 1; j < nums.length; j++) {
for (int k = j + 1; k < nums.length; k++) {
if (nums[k] < 0) {
continue;
}
int a = nums[i];
int c = nums[k];
int b = nums[j];
if (a + b + c == 0) {
tempList.add(a);
tempList.add(b);
tempList.add(c);
set.add(tempList);
}
if (!tempList.isEmpty()) {
tempList = new ArrayList<>();
}
}
}
}
return new ArrayList<>(set);
}
上面代码优化空间已经不大了。后面想到第二种思路,就是分析数据特征,三个数字加起来为0,总感觉很多移动是低效的,例如排序的数组,p1指向的值如果大于0,那后面的循环毫无意义。同理p3指向的值小于0,那也可以直接滑动指针到正数部分。
这种思路:
定义三个指针,依旧排序,头还是不动,后面两个指针分别从头和尾巴向中间靠拢。如果三个数加起来的和<0.左边右移动,如果>0,右边向左移动。如果相等,两者都移动,注意这里有一个优化点就是要一次性跳过相同的数字到下一个不同的数字,否则会浪费耗时。
依照这个思想,最终写出来的版本:
public static List<List<Integer>> threeSumV2(int[] nums) {
// 排序
Arrays.sort(nums);
List<List<Integer>> list = new ArrayList<>();
if (nums.length < 3 || nums[0] > 0 || nums[nums.length - 1] < 0) {
return list;
}
// 固定左边一个,后面滑动
int len = nums.length;
for (int i = 0; i < len - 2; i++) {
// 获取首尾标
int preIndex = i + 1;
int tailIndex = len - 1;
while (preIndex < tailIndex) {
int x = nums[i] + nums[preIndex] + nums[tailIndex];
if (x == 0) {
List<Integer> integers = Arrays.asList(nums[i], nums[preIndex], nums[tailIndex]);
list.add(integers);
if (tailIndex - preIndex > 1) {
// 滑到不同的的数值
while (tailIndex > preIndex + 1 && nums[preIndex] == nums[preIndex + 1]) {
preIndex ++;
}
preIndex ++;
while (tailIndex > preIndex + 1 && nums[tailIndex] == nums[tailIndex - 1]) {
tailIndex --;
}
tailIndex --;
} else {
break;
}
} else if (x > 0) {
tailIndex --;
} else {
preIndex ++;
}
}
// 去重
}
HashSet<List<Integer>> hashSet = new HashSet<>(list);
return new ArrayList<>(hashSet);
}
最开始存放进去去重是list.contains方法,每次循环里面去判断是否存在,并且去重,这样耗时依旧很大,后来不去重,而是在最后ArrayList<>(hashSet)去重复,可以满足耗时了。
执行用时 : 140 ms, 在3Sum的Java提交中击败了24.87% 的用户
内存消耗 : 57.7 MB, 在3Sum的Java提交中击败了51.52% 的用户
给定两个字符串 s1 和 s2,写一个函数来判断 s2 是否包含 s1 的排列。
换句话说,第一个字符串的排列之一是第二个字符串的子串。
示例1:
输入: s1 = “ab” s2 = “eidbaooo”
输出: True
解释: s2 包含 s1 的排列之一 (“ba”).
示例2:
输入: s1= “ab” s2 = “eidboaoo”
输出: False
注意:
输入的字符串只包含小写字母
两个字符串的长度都在 [1, 10,000] 之间
这也是一道很经典题目。很多时候下意识想到的解决方案不一定是错的,但往往不是最佳的。所以停下来思考更好的解决方案往往事半功倍。
这个最开始想到的是暴力破解法。就是找到前面字符串的全排列,然后看是不是被第二个字符串contains。这其实复杂度非常高。
后来我又想到一种思路,就是思考什么是字串,长度肯定是固定的,因此想到了滑动窗口的思想。我只要滑动被匹配的字符串,然后判读是不是该窗口的一种排列组合即可。但是最开始仍然没理解字串到底是什么,所以还是用了递归的思想去匹配窗口的字符串,导致写出来的代码如下:
// 字符串的排列 (耗时过高)
public static boolean checkInclusion(String s1, String s2) {
long start = System.currentTimeMillis();
// 窗口滑动,递归太大
int len = s1.length();
Map<Character, Integer> charCountMap = new HashMap<>();
for (char c : s1.toCharArray()) {
charCountMap.put(c, charCountMap.get(c) == null ? 1 : charCountMap.get(c) + 1);
}
for (int i = 0; i <= s2.length() - len; i++) {
// 加快滑动
String tempStr = s2.substring(i, i + len);
if (!countAndTypeCheck(tempStr, charCountMap)) {
continue;
}
if (isMatch(s1, tempStr)) {
long end = System.currentTimeMillis();
System.out.println(end - start);
return true;
}
}
long end = System.currentTimeMillis();
System.out.println(end - start);
return false;
}
public static boolean isMatch(String s1, String s2) {
if (s2.length() < s1.length()) {
return false;
}
char[] array1 = s1.toCharArray();
// 不使用map,避免stack过深入
Map<Character, Integer> s1CharCountMap = new HashMap<>();
for (char c : array1) {
s1CharCountMap.put(c, s1CharCountMap.get(c) == null ? 1 : s1CharCountMap.get(c) + 1);
}
char[] array2 = s2.toCharArray();
Map<Character, Integer> s2CharCountMap = new HashMap<>();
for (char c : array2) {
s2CharCountMap.put(c, s2CharCountMap.get(c) == null ? 1 : s2CharCountMap.get(c) + 1);
}
// 指针
Map<Character, Integer> tempCounterMap = new HashMap<>(s1CharCountMap);
// 累计
int count = 0;
int subIndex = 0;
for (int i = 0; i < array2.length; i++) {
char c = array2[i];
if (tempCounterMap.get(c) != null && tempCounterMap.get(c) > 0) {
// 还有数量
count ++;
tempCounterMap.put(c, tempCounterMap.get(c) - 1);
if (subIndex == 0) {
subIndex = i + 1;
}
} else {
if (count == s1.length()) {
return true;
}
tempCounterMap = new HashMap<>(s1CharCountMap);
count = 0;
}
}
if (count == s1.length()) {
return true;
}
return isMatch(s1, s2.substring(subIndex == 0 ? 1 : subIndex));
}
后来复杂度依旧非常高。后来再仔细想字串不就是满足两点即可了吗:
- 长度相等
- 每个字符的个数相等
随即优化如下:
public static boolean checkInclusionV2(String s1, String s2) {
// 窗口滑动,递归太大
int len = s1.length();
Map<Character, Integer> charCountMap = new HashMap<>();
for (char c : s1.toCharArray()) {
charCountMap.put(c, charCountMap.get(c) == null ? 1 : charCountMap.get(c) + 1);
}
for (int i = 0; i <= s2.length() - len; i++) {
// 加快滑动
String tempStr = s2.substring(i, i + len);
if (countAndTypeCheck(tempStr, charCountMap)) {
return true;
}
}
return false;
}
// 种类和数量判断
private static boolean countAndTypeCheck(String tempStr, Map<Character, Integer> charCountMap) {
char[] chars = tempStr.toCharArray();
for (char c : chars) {
int count = 0;
Integer integer = charCountMap.get(c) == null ? 0 : charCountMap.get(c);
for (char c2 : chars) {
if (c2 == c) {
count ++;
}
}
if (integer != count) {
return false;
}
}
return true;
}
不要急着写代码,多画草图再动手。
给定一个包含了一些 0 和 1的非空二维数组 grid , 一个 岛屿 是由四个方向 (水平或垂直) 的 1 (代表土地) 构成的组合。你可以假设二维矩阵的四个边缘都被水包围着。
找到给定的二维数组中最大的岛屿面积。(如果没有岛屿,则返回面积为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。
由于
可以想到这是个递归的问题,这个问题对于我来说的难点在于如何去控制指针不走回头路。后来想了很久,把走过的路径设置为0即可。
遍历坐标,如果该坐标的值是1,就把当前位置设置为0,并判断上下左右的值是否是1,如果是就把坐标移动到该位置进行递归。这里需要注意的点是如果避免外层遍历去跳过已经走过的坐标,我用了两个数组,一个去保存全局足迹,一个去保存最大足迹。尽可能去降低复杂度。
代码:
public static int maxAreaOfIsland(int[][] grid) {
// 定义最大的岛屿值
int count = 0;
// 走过的坐标
List<String> tracedLocation = new ArrayList<>();
for (int i = 0; i < grid.length; i++) {
for (int j = 0; j < grid[i].length; j++) {
List<String> maxIslandLocation = new ArrayList<>();
if (grid[i][j] == 1 && ! skipLocation(i, j, tracedLocation)) {
countIsland(i, j, grid, tracedLocation, maxIslandLocation);
if (count < maxIslandLocation.size()) {
count = maxIslandLocation.size();
} else {
maxIslandLocation.clear();
}
}
}
}
return count;
}
/**
* 1 1 0 0
* 1 1 0 0
* 0 0 1 1
* 0 0 1 1
*
* 0 1
* 1 1
* @param i
* @param j
* @param grid
* @param tracedLocation
* @param maxIslandLocation
*/
// 计算该坐标周围的岛屿面积
private static void countIsland(int i, int j, int[][] grid, List<String> tracedLocation, List<String> maxIslandLocation) {
String location = getLocation(i, j);
tracedLocation.add(location);
maxIslandLocation.add(location);
grid[i][j] = 0;
// 上
if (i > 0 && grid[i - 1][j] == 1) {
countIsland(i - 1, j, grid, tracedLocation, maxIslandLocation);
}
// 下
if (i + 1 < grid.length && grid[i + 1][j] == 1) {
countIsland(i + 1, j, grid, tracedLocation, maxIslandLocation);
}
// 左
if (j > 0 && grid[i][j - 1] == 1) {
countIsland(i, j - 1, grid, tracedLocation, maxIslandLocation);
}
// 右
if (j + 1 < grid[i].length && grid[i][j + 1] == 1) {
countIsland(i, j + 1, grid, tracedLocation, maxIslandLocation);
}
}
// 跳过岛屿已经数过的坐标
private static boolean skipLocation(int i, int j, List<String> tracedLocation) {
return tracedLocation.contains(getLocation(i, j));
}
private static String getLocation(int i, int j) {
StringBuffer location = new StringBuffer();
location.append(i).append("_").append(j);
return location.toString();
}
给定一个排序链表,删除所有含有重复数字的节点,只保留原始链表中 没有重复出现 的数字。
示例 1:
输入: 1->2->3->3->4->4->5
输出: 1->2->5
示例 2:
输入: 1->1->1->2->3
输出: 2->3
Find the nodes by three pointer
This coding test turn out in bytedance online interview.
The key is to analazy the feature every matchs node.
thinking slow down, when you aware the features , the result is turn out.
public static ListNode deleteDuplicates(ListNode head) {
// check the null
if (head == null) {
return null;
}
/**
* define three pointer
* pre stand pre node
* current stand current node
* next stand next node
*/
ListNode pre = null;
ListNode current = head;
ListNode next = head.next;
ListNode p = null;
ListNode newPre = null;
while (current != null) {
/**
* juding the node is match critical point below
* frist of all , check pre is null or pre val not equals
* then check next is null or val not equals
*
* notes : header and tail need to consider
*/
if ((pre == null || pre.val != current.val) && (next == null || next.val != current.val)) {
// current node is match
// head processed
if (p == null) {
p = current;
newPre = p;
} else {
p.next = current;
p = p.next;
}
// to release the pointer to next
p.next = null;
}
/**
* slip the three pointer
*/
pre = current;
current = next;
next = current != null ? current.next : null;
}
return newPre;
}
将一个给定字符串根据给定的行数,以从上往下、从左到右进行 N字形排列。
比如输入字符串为 “LEETCODEISHIRING” 行数为 3 时,排列如下:
L C I R
E T O E S I I G
E D H N
之后,你的输出需要从左往右逐行读取,产生出一个新的字符串,比如:“LCIRETOESIIGEDHN”。
请你实现这个将字符串进行指定行数变换的函数:
string convert(string s, int numRows);
示例 1:
输入: s = “LEETCODEISHIRING”, numRows = 3
输出: “LCIRETOESIIGEDHN”
示例 2:
输入: s = “LEETCODEISHIRING”, numRows = 4
输出: “LDREOEIIECIHNTSG”
解释:
L D R
E O E I I
E C I H N
T S G
思路:最开始打算分析数组的长度,后来发现并不好分析,推导出来一个公式,后来才发现N形并不是完整的,可能是一半,所以公式不实用,后来重新分析二维数组,初始化为最大的二维数组,二维数组宽度的上线也就是字符串的长度了。相信会有更好的优化方式。然后分析每个坐标的特征。注意当字符串输出后,整个循环需要提前终止,否则会导致字符串越界。
public static String convert(String s, int numRows) {
if (numRows == 1) {
return s;
}
// initial the array
/**
* I can't confirm the multi array witdh, so initial it by max possible
*/
int max = s.length() > numRows ? s.length() : numRows;
String[][] array = new String[max][max];
// this number feature in array
/**
* this number is width refer to features
*/
int n = numRows - 1;
// index to record string slip
int index = 0;
/**
* if the slip to string end , loop should be break
*/
boolean isEnd = false;
for (int j = 0; j < max; j++) {
for (int i = 0; i < numRows; i++) {
if (index >= s.length()) {
isEnd = true;
break;
}
// here is the key, add the location
if ((j % n == 0) || (i + j) % n == 0) {
array[i][j] = String.valueOf(s.charAt(index));
index ++;
}
}
// if string is end, break out the outter loop
if (isEnd) {
break;
}
}
/**
* append the result from multi array
*/
String result = "";
for (int i = 0; i < array.length; i++) {
for (int j = 0; j < array[i].length; j++) {
if (array[i][j] != null) {
result += array[i][j];
}
}
}
return result;
}
给定一个整数数组 nums 和一个目标值 target,请你在该数组中找出和为目标值的那 两个 整数,并返回他们的数组下标。
你可以假设每种输入只会对应一个答案。但是,你不能重复利用这个数组中同样的元素。
示例:
给定 nums = [2, 7, 11, 15], target = 9
因为 nums[0] + nums[1] = 2 + 7 = 9
所以返回 [0, 1]
思路:很简单直接的方式即时暴力法,这个就是两层遍历。但是复杂度比较高,时间复杂度是n平方。
public int[] twoSum(int[] nums, int target) {
for (int i = 0; i < nums.length - 1; i++) {
for (int j = i + 1; j < nums.length; j++) {
if (nums[i] + nums[j] == target) {
return new int[]{i, j};
}
}
}
return arr;
}
为了优化,可以采用hash表的方式,一次遍历即可。每次判断被减数是否存在hash表中。把原先的查询从for循环中变成了Hash表的方式,只要Key不冲突太厉害,时间可以降低到1。这个版本如下:
/**
* Hash增强版本
* @param nums
* @param target
* @return
*/
public static int[] twoSum2(int[] nums, int target) {
Map<Integer, Integer> map = new HashMap<>();
for (int i = 0; i < nums.length; i++) {
int numberTwo = target - nums[i];
if (map.containsKey(numberTwo)) {
return new int[]{i, map.get(numberTwo)};
}
map.put(nums[i], i);
}
return null;
}
最后
以上就是兴奋小霸王为你收集整理的【leetcode】算法系列的全部内容,希望文章能够帮你解决【leetcode】算法系列所遇到的程序开发问题。
如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。
发表评论 取消回复