文章预览
一、 数组
1 二维数组
矩阵中的路径(是否包含)
输入:board = [[“A”,“B”,“C”,“E”],[“S”,“F”,“C”,“S”],[“A”,“D”,“E”,“E”]], word = “ABCCED”
输出:true
思路:回溯,DFS
class Solution {
public boolean exist(char[][] board, String word) {
char[] words = word.toCharArray();
for(int i = 0; i < board.length; i++){
for(int j = 0; j < board[0].length;j++){
if(dfs(board,words,i,j,0)){
return true;
}
}
}
return false;
}
public boolean dfs(char[][] board, char[] words, int i, int j, int p){
//是否越界或者字符不匹配
if(i < 0 || i >= board.length || j < 0 || j >= board[0].length || board[i][j] != words[p]){
return false;
}
if(p == words.length - 1){
return true;
}
char tmp = board[i][j];
board[i][j] = '#'; //标记已访问过
//上下左右找
boolean res = dfs(board,words,i-1,j,p+1) || dfs(board,words,i+1,j,p+1)
|| dfs(board,words,i,j-1,p+1) || dfs(board,words,i,j+1,p+1);
board[i][j] = tmp;//去除标记
return res;
}
}
搜索二维矩阵
题目:
编写一个高效的算法来搜索 m x n 矩阵 matrix 中的一个目标值 target 。该矩阵具有以下特性:
每行的元素从左到右升序排列。
每列的元素从上到下升序排列。
思路:从右上角开始找
public boolean searchMatrix(int[][] matrix, int target) {
//从矩阵右上角开始搜索
int col = matrix[0].length - 1;//列
int row = 0;//行
while (col >= 0 && row <= matrix.length - 1) {
if (target == matrix[row][col]) {
//如果找到就直接返回
return true;
} else if (target < matrix[row][col]) {
//如果查找的值大了,下一步往左找
col--;
} else if (target > matrix[row][col]) {
//如果查找的值小了,下一步往下找
row++;
}
}
return false;
}
顺时针打印矩阵
class Solution {
public int[] spiralOrder(int[][] matrix) {
//特判
if(matrix == null ||matrix.length == 0){
return new int[0];
}
//初始化
int left = 0, top = 0;
int right = matrix[0].length-1;
int bottom = matrix.length - 1;
int[] res = new int[(right+1)*(bottom+1)];
int k = 0;
//循环打印
while(top <= bottom && left <= right){
for(int i = left; i <= right; i++){
//从左到右
res[k++] = matrix[top][i];
}
top ++;
for(int i = top; i <= bottom; i++){
//从上到下
res[k++] = matrix[i][right];
}
right --;
for(int i = right; i >= left && top <= bottom; i--){
//从右到左
res[k++] = matrix[bottom][i];
}
bottom --;
for(int i = bottom; i >= top && left <= right; i--){
//从下到上
res[k++] = matrix[i][left];
}
left ++;
}
return res;
}
}
2. 排序数组
合并排序的数组
class Solution {
public void merge(vector<int>& A, int m, vector<int>& B, int n) {
int l=m+n-1;
int a=m-1;
int b=n-1;
while(a>=0&&b>=0){
if(A[a]>=B[b])
A[l--]=A[a--];
else
A[l--]=B[b--];
}
while(b>=0)
A[l--]=B[b--];
}
}
在排序数组中查找元素的第一个和最后一个位置(重复)(二分)
题目:给定一个按照升序排列的整数数组 nums,和一个目标值 target。找出给定目标值在数组中的开始位置和结束位置。如果数组中不存在目标值 target,返回 [-1, -1]。
思路:二分法,找target第一次出现的位置,和最后一次出现的位置
public int[] searchRange(int[] nums, int target) {
int first = searchFirst(nums, target);
//判断有没有查找到
if (first < nums.length && nums[first] == target) {
int last = searchLast(nums, target);
return new int[]{
first, last};
} else {
//没有找到这个值,直接返回{-1, -1}
return new int[]{
-1, -1};
}
}
//如果数组nums中有多个target,那么返回值就是第一个出现的target的下标
//如果数组nums中没有target,那么返回的就是第一个大于target的下标
public static int searchFirst(int[] nums, double target) {
int low = 0, high = nums.length - 1;
while (low <= high) {
int m = low + (high - low) / 2;
if (target > nums[m])
low = m + 1;
else
high = m - 1;
}
return low;
}
public static int searchLast(int[] nums, double target) {
int low = 0, high = nums.length - 1;
while (low <= high) {
int m = low + (high - low) / 2;
if (target >= nums[m])
low = m + 1;
else
high = m - 1;
}
return high;
}
时间复杂度: O(logn) ,其中 n 为数组的长度。二分查找的时间复杂度为 O(logn),一共会执行两次,因此总时间复杂度为 O(logn)。
空间复杂度:O(1)。只需要常数空间存放若干变量。
在排序数组中查找数字出现的次数(二分)
class Solution {
public int search(int[] nums, int target) {
// 搜索右边界 right 即最后一次出现的下标
int i = 0, j = nums.length - 1;
while(i <= j) {
int m = (i + j) / 2;
if(nums[m] <= target) i = m + 1;
else j = m - 1;
}
int right = i;
// 若数组中无 target ,则提前返回
if(j >= 0 && nums[j] != target) return 0;
// 搜索左边界 right 第一次出现的下标
i = 0; j = nums.length - 1;
while(i <= j) {
int m = (i + j) / 2;
if(nums[m] < target) i = m + 1;
else j = m - 1;
}
int left = j;
return right - left - 1;
}
}
奇前偶后
public static int[] exchange2(int[] nums) {
int left = 0,right = nums.length-1;
while(left<right) {
if(nums[left]%2==0&&nums[right]%2==1) {
int temp = nums[left];
nums[left] = nums[right];
nums[right] = temp;
}else if (nums[left]%2==1) {
left++;
}else if (nums[right]%2==0) {
right--;
}
}
return nums;
}
4. 全排列
下一个排列
题目:实现获取 下一个排列 的函数,算法需要将给定数字序列重新排列成字典序中下一个更大的排列。
如果不存在下一个更大的排列,则将数字重新排列成最小的排列(即升序排列)。必须 原地 修改,只允许使用额外常数空间。
思路:
1.先找出最大的索引 k 满足 nums[k] < nums[k+1],如果不存在,就翻转整个数组;
2.再找出另一个最大索引 l 满足 nums[l] > nums[k];
3.交换 nums[l] 和 nums[k];
4.最后翻转 nums[k+1:]
class Solution {
public void nextPermutation(int[] nums) {
if (nums == null || nums.length == 0) return;
int firstIndex = -1;
for (int i = nums.length - 2; i >= 0; i--) {
if (nums[i] < nums[i + 1]) {
firstIndex = i;
break;
}
}
if (firstIndex == -1) {
reverse(nums, 0, nums.length - 1);
return;
}
int secondIndex = -1;
for (int i = nums.length - 1; i >= 0; i--) {
if (nums[i] > nums[firstIndex]) {
secondIndex = i;
break;
}
}
swap(nums, firstIndex, secondIndex);
reverse(nums, firstIndex + 1, nums.length - 1);
return;
}
private void reverse(int[] nums, int i, int j) {
while (i < j) {
swap(nums, i++, j--);
}
}
全排列
题目:给定一个 没有重复 数字的序列,返回其所有可能的全排列。
思路:回溯
class Solution {
public List<List<Integer>> permute(int[] nums) {
List<List<Integer>> res = new ArrayList<>();
int[] visited = new int[nums.length];
backtrack(res, nums, new ArrayList<Integer>(), visited);
return res;
}
private void backtrack(List<List<Integer>> res, int[] nums, ArrayList<Integer> tmp, int[] visited) {
if (tmp.size() == nums.length) {
res.add(new ArrayList<>(tmp));
return;
}
for (int i = 0; i < nums.length; i++) {
if (visited[i] == 1) continue;
visited[i] = 1;
tmp.add(nums[i]);
backtrack(res, nums, tmp, visited);
visited[i] = 0;
tmp.remove(tmp.size() - 1);
}
}
}
3. 旋转数组
旋转数组的最小数字
思考两个问题
满足什么条件可以确定最小值在右边?
如果索引mid处的值比right处的值要大,说明mid在左半边的升序数组,最小值在mid右边,令left = mid + 1
满足什么条件可以确定最小值在左边?
如果索引mid处的值比right处的值要小,说明mid在右半边的升序数组,最小值可能就在mid处,也可能在mid左边,令right =mid 同样,如果索引mid处的值比left处的值要小,也可以说明mid在右半边的升序数组,同样令right = mid
这两个问题解决后,还要解决最后一个问题,因为数组中可以有重复元素,如果mid处的值和right处的值一样怎么办?这种情况下,mid和right所指的数都有可能是最小值,既然两个都可能是最小值,只要保留一个在查找区间内就可以了,把mid保留,令right = right – 1
class Solution:
def minArray(self, numbers: List[int]) -> int:
left = 0
right = len(numbers) - 1
while left <= right:
mid = (left + right) // 2
if numbers[mid] > numbers[right]://mid在左边的升序中
left = mid + 1
elif numbers[mid] < numbers[right]://mid在右边的升序中
right = mid
else:
right -= 1
return numbers[left]
搜索旋转排序数组(不重复)(二分查找)
题目:给你 旋转后 的数组 nums 和一个整数 target ,如果 nums 中存在这个目标值 target ,则返回它的下标,否则返回 -1 。
思路:二分查找
public int search(int[] nums, int target) {
int n = nums.length;
int left = 0, right = n - 1;
while(left <= right){
int mid = (right - left) / 2 + left;
if(nums[mid] == target)
return mid;
if(nums[left] <= nums[mid]){
// [0...mid]左有序
if(target >= nums[left] && target < nums[mid]){
right = mid - 1;
}
else{
left = mid + 1;
}
}
else{
// [mid...n-1]右有序
if(target > nums[mid] && target <= nums[n-1]){
left = mid + 1;
}
else{
right = mid - 1;
}
}
}
return -1;
}
时间复杂度:O(logN),这里 N 是数组的长度,在循环中一次排除一半,因此时间复杂度是对数级别的。
空间复杂度:O(1),使用到的临时变量的个数是常数。
搜索旋转排序数组(重复)
题目:若存在target返回true , 否则返回false。
思路: 对于数组中有重复元素的情况,二分查找时可能会有 a[l]=a[mid]=a[r],此时无法判断区间 [l,mid]和区间[mid+1,r] 哪个是有序的。
例如nums=[3,1,2,3,3,3,3],target=2,首次二分时无法判断区间 [0,3]和区间 [4,6]哪个是有序的。
对于这种情况,我们只能将当前二分区间的左边界加一,右边界减一,然后在新区间上继续二分查找。
class Solution {
public boolean search(int[] nums, int target) {
int n = nums.length;
if (n == 0) {
return false;
}
if (n == 1) {
return nums[0] == target;
}
int left = 0, right = n - 1;
while (left <= right) {
int mid = (left + right) / 2;
if (nums[mid] == target) {
return true;
}
if (nums[left] == nums[mid] && nums[mid] == nums[right]) {
++l;
--r;
} else if (nums[left] <= nums[mid]) {
if (nums[left] <= target && target < nums[mid]) {
right = mid - 1;
} else {
left = mid + 1;
}
} else {
if (nums[mid] < target && target <= nums[n - 1]) {
left = mid + 1;
} else {
right = mid - 1;
}
}
}
return false;
}
}
字母异位词分组
题目:给定一个字符串数组,将字母异位词组合在一起。字母异位词指字母相同,但排列不同的字符串。
输入: [“eat”, “tea”, “tan”, “ate”, “nat”, “bat”]
输出:
[
[“ate”,“eat”,“tea”],
[“nat”,“tan”],
[“bat”]
]
class Solution {
public List<List<String>> groupAnagrams(String[] strs) {
Map<String, List<String>> map = new HashMap<String, List<String>>();
for (String str : strs) {
char[] array = str.toCharArray();
Arrays.sort(array);
String key = new String(array);
List<String> list = map.getOrDefault(key, new ArrayList<String>());
list.add(str);
map.put(key, list);
}
return new ArrayList<List<String>>(map.values());
}
}
时间复杂度:O(nklogk),其中 n 是 strs 中的字符串的数量,k 是 strs 中的字符串的的最大长度。需要遍历 n 个字符串,对于每个字符串,需要O(klogk) 的时间进行排序以及 O(1) 的时间更新哈希表,因此总时间复杂度是 O(nklogk)。
空间复杂度:O(nk),其中 n 是 strs 中的字符串的数量,k 是 strs 中的字符串的的最大长度。需要用哈希表存储全部字符串。
合并区间
题目:以数组 intervals 表示若干个区间的集合,其中单个区间为 intervals[i] = [starti, endi] 。请你合并所有重叠的区间,并返回一个不重叠的区间数组,该数组需恰好覆盖输入中的所有区间。
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Stack;
public class Solution {
public int[][] merge(int[][] intervals) {
int len = intervals.length;
if (len < 2) {
return intervals;
}
// 按照起点排序
Arrays.sort(intervals, (v1, v2) -> v1[0] - v2[0]);
List<int[]> res = new ArrayList<>();
res.add(intervals[0]);
for (int i = 1; i < len; i++) {
int[] curInterval = intervals[i];
// 每次新遍历到的列表与当前结果集中的最后一个区间的末尾端点进行比较
int[] peek = res.get(res.size() - 1);
if (curInterval[0] > peek[1]) {
res.add(curInterval);
} else {
// 注意,这里应该取最大
peek[1] = Math.max(curInterval[1], peek[1]);
}
}
return res.toArray(new int[res.size()][]);
}
public static void main(String[] args) {
Solution solution = new Solution();
int[][] intervals = {
{
1, 3}, {
2, 6}, {
8, 10}, {
15, 18}};
int[][] res = solution.merge(intervals);
for (int i = 0; i < res.length; i++) {
System.out.println(Arrays.toString(res[i]));
}
}
}
时间复杂度:O(nlogn),其中 n 为区间的数量。除去排序的开销,我们只需要一次线性扫描,所以主要的时间开销是排序的O(nlogn)。
空间复杂度:O(logn),其中 n 为区间的数量。这里计算的是存储答案之外,使用的额外空间。O(logn) 即为排序所需要的空间复杂度。
编辑距离
题目:给你两个单词 word1 和 word2,请你计算出将 word1 转换成 word2 所使用的最少操作数 。你可以对一个单词进行如下三种操作:
插入一个字符
删除一个字符
替换一个字符
思路:动态规划
class Solution {
public int minDistance(String word1, String word2) {
int n1 = word1.length();
int n2 = word2.length();
int[][] dp = new int[n1 + 1][n2 + 1];
//第一行,是 word1 为空变成 word2 最少步数,就是插入操作
//第一列,是 word2 为空,需要的最少步数,就是删除操作
// 第一行
for (int j = 1; j <= n2; j++) dp[0][j] = dp[0][j - 1] + 1;
// 第一列
for (int i = 1; i <= n1; i++) dp[i][0] = dp[i - 1][0] + 1;
for (int i = 1; i <= n1; i++) {
for (int j = 1; j <= n2; j++) {
if (word1.charAt(i - 1) == word2.charAt(j - 1)) dp[i][j] = dp[i - 1][j - 1];
else dp[i][j] = Math.min(Math.min(dp[i - 1][j - 1], dp[i][j - 1]), dp[i - 1][j]) + 1;
}
}
return dp[n1][n2];
}
}
(一)、当word1[i]==word2[j]时,由于遍历到了i和j,说明word1的0i-1和word2的0j-1的匹配结果已经生成,由于当前两个字符相同,因此无需做任何操作,dp[i][j]=dp[i-1][j-1]
(二)、当word1[i]!=word2[j]时,可以进行的操作有3个:
① 替换操作:可能word1的0i-1位置与word2的0j-1位置的字符都相同,
只是当前位置的字符不匹配,进行替换操作后两者变得相同,
所以此时dp[i][j]=dp[i-1][j-1]+1(这个加1代表执行替换操作)
②删除操作:若此时word1的0i-1位置与word2的0j位置已经匹配了,
此时多出了word1的i位置字符,应把它删除掉,才能使此时word1的0~i(这个i是执行了删除操作后新的i)
和word2的0~j位置匹配,因此此时dp[i][j]=dp[i-1][j]+1(这个加1代表执行删除操作)
③插入操作:若此时word1的0i位置只是和word2的0j-1位置匹配,
此时只需要在原来的i位置后面插入一个和word2的j位置相同的字符使得
此时的word1的0i(这个i是执行了插入操作后新的i)和word2的0j匹配得上,
所以此时dp[i][j]=dp[i][j-1]+1(这个加1代表执行插入操作)
④由于题目所要求的是要最少的操作数:所以当word1[i] != word2[j] 时,
需要在这三个操作中选取一个最小的值赋格当前的dp[i][j]
(三)总结:状态方程为:
if(word1[i] == word2[j]):
dp[i][j] = dp[i-1][j-1]
else:
min(dp[i-1][j-1],dp[i-1][j],dp[i][j-1])+1
PS:大佬的代码中word1.charAt(i-1)==word2.charAt(j-1)的原因是:
初始化DP Table时dp[i][0]和dp[0][j]已经填写完成,所以接下来填表需要从1开始,
但是字符的比较需要从0开始,因此才这样子写
时间复杂度 :O(mn),其中 m 为 word1 的长度,n 为 word2 的长度。
空间复杂度 :O(mn),我们需要大小为O(mn) 的 D 数组来记录状态值。
子集
题目:给你一个整数数组 nums ,数组中的元素 互不相同 。返回该数组所有可能的子集(幂集)。解集 不能 包含重复的子集。你可以按 任意顺序 返回解集。
思路:递归(回溯算法)
class Solution {
public List<List<Integer>> subsets(int[] nums) {
List<List<Integer>> res = new ArrayList<>();
backtrack(0, nums, res, new ArrayList<Integer>());
return res;
}
private void backtrack(int i, int[] nums, List<List<Integer>> res, ArrayList<Integer> tmp) {
res.add(new ArrayList<>(tmp));
for (int j = i; j < nums.length; j++) {
tmp.add(nums[j]);
backtrack(j + 1, nums, res, tmp);
tmp.remove(tmp.size() - 1);
}
}
}
- 最长连续序列
乘积最大子数组
题目:给你一个整数数组 nums ,请你找出数组中乘积最大的连续子数组(该子数组中至少包含一个数字),并返回该子数组所对应的乘积。
思路:动态规划
累乘的乘积等于 0,就要重新开始
累乘的乘积小于 0,要找到前面最大的负数,这样才能保住从 i 到 j 最大
累乘的乘积大于 0,要找到前面最小的正数,同理!
class Solution {
public int maxProduct(int[] nums) {
if (nums == null || nums.length == 0) return 0;
int res = nums[0];
int pre_max = nums[0];
int pre_min = nums[0];
for (int i = 1; i < nums.length; i++) {
int cur_max = Math.max(Math.max(pre_max * nums[i], pre_min * nums[i]), nums[i]);
int cur_min = Math.min(Math.min(pre_max * nums[i], pre_min * nums[i]), nums[i]);
res = Math.max(res, cur_max);
pre_max = cur_max;
pre_min = cur_min;
}
return res;
}
}
5. TopK问题
最小的K个数
由于每次从堆顶弹出的数都是堆中最大的,最小的 k 个元素一定会留在堆里
public int[] getLeastNumbers(int[] arr, int k) {
if (k == 0) {
return new int[0];
}
// 使用一个最大堆(大顶堆)
// Java 的 PriorityQueue 默认是小顶堆,添加 comparator 参数使其变成最大堆
Queue<Integer> heap = new PriorityQueue<>(k, (i1, i2) -> Integer.compare(i2, i1));
for (int e : arr) {
// 当前数字小于堆顶元素才会入堆
if (heap.isEmpty() || heap.size() < k || e < heap.peek()) {
heap.offer(e);
}
if (heap.size() > k) {
heap.poll(); // 删除堆顶最大元素
}
}
// 将堆中的元素存入数组
int[] res = new int[heap.size()];
int j = 0;
for (int e : heap) {
res[j++] = e;
}
return res;
}
数组中的第K个最大元素(小根堆、大根堆!!!)
思路:
- 建个数为k的小顶堆
这k个数是数组中最大的k个数,则堆顶就是第K个最大元素
2.大根堆:建立一个大根堆,做 k – 1k−1 次删除操作后堆顶元素就是我们要找的答案
/** 小根堆 */
class Solution2 {
public int findKthLargest(int[] nums, int k) {
//前K个元素原地建小顶堆
buildHeap(nums, k);
//遍历剩下元素,比堆顶小,跳过;比堆顶大,交换后重新堆化
for (int i = k; i < nums.length; i++) {
if (nums[i] < nums[0]) continue;
swap(nums, i, 0);
heapify(nums, k, 0);
}
//K个元素的小顶堆的堆顶即是第K大元素
return nums[0];
}
/** * 建堆函数 * 从倒数第一个非叶子节点开始堆化,倒数第一个非叶子节点下标为 K/2-1 */
public void buildHeap(int[] a, int k) {
for (int i = k/2 - 1; i >= 0; i--) {
heapify(a, k, i);
}
}
/** * 堆化函数 * 父节点下标i,左右子节点的下标分别为 2*i+1 和 2*i+2 */
public void heapify(int[] a, int k, int i) {
//临时变量 minPos 用于存储最小值的下标,先假设父节点最小
int minPos = i;
while (true) {
//和左子节点比较
if (i*2+1 < k && a[i*2+1] < a[i]) minPos = i*2+1;
//和右子节点比较
if (i*2+2 < k && a[i*2+2] < a[minPos]) minPos = i*2+2;
//如果minPos没有发生变化,说明父节点已经是最小了,直接跳出
if (minPos == i) break;
//否则交换
swap(a, i, minPos);
//父节点下标进行更新,继续堆化
i = minPos;
}
}
public void swap(int[] a, int n, int m) {
int tmp = a[n];
a[n] = a[m];
a[m] = tmp;
}
}
//大根堆
class Solution {
public int findKthLargest(int[] nums, int k) {
if(nums == null){
return 0;
}
// 建立一个大顶堆
for (int i = nums.length - 1; i >= 0; i--){
heapFiy(i, nums, nums.length);
}
int heapSize = nums.length;
swap(nums,0, --heapSize);
int kCount = 1;
if(k==1){
//heapFiy(0,nums,heapSize);
return nums[heapSize];
}
while(heapSize>0){
kCount++;
heapFiy(0,nums,heapSize);
if(kCount>=k){
return nums[0];
}
swap(nums,0, --heapSize);
}
return 0;
}
public void heapFiy(int index,int [] nums,int heapSize){
int left = index*2 +1;
while(left < heapSize){
int largest = ((left+1) < heapSize && nums[left+1] > nums[left]) ? left+1:left;
largest = nums[largest] > nums[index] ? largest : index;
if (largest == index){
break;
}
swap(nums,largest,index);
index = largest;
left = index*2 + 1;
}
}
public void swap(int[] nums,int left,int right){
int temp = nums[left];
nums[left] = nums[right];
nums[right] = temp;
}
}
- 完全平方数
题目:给定正整数 n,找到若干个完全平方数(比如 1, 4, 9, 16, …)使得它们的和等于 n。你需要让组成和的完全平方数的个数最少。
给你一个整数 n ,返回和为 n 的完全平方数的 最少数量 。
寻找重复数
题目:给定一个包含 n + 1 个整数的数组 nums ,其数字都在 1 到 n 之间(包括 1 和 n),可知至少存在一个重复的整数。
假设 nums 只有 一个重复的整数 ,找出 这个重复的数 。
class Solution {
//leecode 287 寻找一个重复的数
//思想用一个额外的空间进行记录
public int findDuplicate(int[] nums) {
int[] dp = new int[nums.length + 1];
for (int i = 0; i < nums.length; i++) {
if (dp[nums[i]] == 1) {
return nums[i];
}
dp[nums[i]]++;
}
return -1;
}
}
最长递增子序列
题目:给你一个整数数组 nums ,找到其中最长严格递增子序列的长度。
子序列是由数组派生而来的序列,删除(或不删除)数组中的元素而不改变其余元素的顺序。例如,[3,6,2,7] 是数组 [0,3,1,6,2,2,7] 的子序列。
思路:动态规划
// Dynamic programming.
class Solution {
public int lengthOfLIS(int[] nums) {
if(nums.length == 0) return 0;
int[] dp = new int[nums.length];
int res = 0;
Arrays.fill(dp, 1); //初始化为1
for(int i = 0; i < nums.length; i++) {
for(int j = 0; j < i; j++) {
if(nums[j] < nums[i]) dp[i] = Math.max(dp[i], dp[j] + 1);
}
res = Math.max(res, dp[i]);
}
return res;
}
}
- 零钱兑换
6. 和问题
两数之和
题目:给定一个整数数组 nums 和一个整数目标值 target,请你在该数组中找出 和为目标值 的那 两个 整数,并返回它们的数组下标。
你可以假设每种输入只会对应一个答案。但是,数组中同一个元素在答案里不能重复出现。
你可以按任意顺序返回答案。
思路:使用哈希表,对于每一个x,我们首先查询哈希表中是否存在 target – x,然后将 x 插入到哈希表中,即可保证不会让 x 和自己匹配。
public int[] twoSum(int[] nums, int target) {
Map<Integer, Integer> hashmap = new HashMap<>();
for(int i = 0; i < nums.length; i++){
if(hashmap.containsKey(target - nums[i])){
return new int[]{
hashmap.get(target - nums[i]), i};
}
hashmap.put(nums[i], i);
}
return null;
}
时间复杂度:O(n)
空间复杂度:O(n)
三数之和
题目:给你一个包含 n 个整数的数组 nums,判断 nums 中是否存在三个元素 a,b,c ,使得 a + b + c = 0 ?请你找出所有和为 0 且不重复的三元组。
思路:首先对数组进行排序,排序后固定一个数 nums[i]nums[i],再使用左右指针指向 nums[i]nums[i]后面的两端,数字分别为 nums[L]nums[L] 和 nums[R]nums[R],计算三个数的和 sumsum 判断是否满足为 00,满足则添加进结果集
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
public class AddThreeNum {
public static void main(String[] args) {
}
public static List<List<Integer>> threeSum(int[] nums) {
Arrays.sort(nums);
List<List<Integer>> res = new ArrayList<>();
for(int k = 0; k < nums.length - 2; k++){
if(nums[k] > 0) break;
if(k > 0 && nums[k] == nums[k - 1]) continue;//跳过重复的
int i = k + 1, j = nums.length - 1;
while(i < j){
int sum = nums[k] + nums[i] + nums[j];
if(sum < 0){
while(i < j && nums[i] == nums[i+1]) i++;
} else if (sum > 0) {
while(i < j && nums[j] == nums[j-1]) j--;
} else {
res.add(new ArrayList<Integer>(Arrays.asList(nums[k], nums[i], nums[j])));
while(i < j && nums[i] == nums[i+1]) i++;
while(i < j && nums[j] == nums[j-1]) j--;
i++;
j--;
}
}
}
return res;
}
}
时间复杂度 O(N^2) .其中固定指针k循环复杂度 O(N),双指针 i,j 复杂度 O(N)O(N)。
空间复杂度 O(1):指针使用常数大小的额外空间。
和为k的子数组
题目:给定一个整数数组和一个整数 k,你需要找到该数组中和为 k 的连续的子数组的个数。
输入:nums = [1,1,1], k = 2
输出: 2 , [1,1] 与 [1,1] 为两种不同的情况。
思路:前缀和
public int subarraySum(int[] nums, int k) {
Map <Integer,Integer> map = new HashMap<>();
//map保存映射 序列和 : 等于该序列和的数量
int sum = 0;//计算求和的值
int ans=0;
map.put(0,1);//初始化,代表:序列和为0的值 初始只有1个。
for(int i=0;i<nums.length;i++){
sum+=nums[i];
//存在从某个子序列和 presum 到 当前的序列和 sum 差值刚好为k的值,
//中间的值必然是连续的
if(map.containsKey(sum-k)){
ans+=map.get(sum-k);
}
//序列和存在的话,设置为原始数量+1,不存在则初始设为0
map.put(sum,map.getOrDefault(sum,0)+1);
}
return ans;
}
组合总和
题目:给定一个无重复元素的数组 candidates 和一个目标数 target ,找出 candidates 中所有可以使数字和为 target 的组合。candidates 中的数字可以无限制重复被选取。
输入:candidates = [2,3,6,7], target = 7,
所求解集为:
[
[7],
[2,2,3]
]
思路:回溯+剪枝
class Solution {
public List<List<Integer>> combinationSum(int[] candidates, int target) {
List<List<Integer>> ans = new ArrayList<List<Integer>>();
List<Integer> combine = new ArrayList<Integer>();
dfs(candidates, target, ans, res, 0);
return ans;
}
public void dfs(int[] candidates, int target, List<List<Integer>> ans, List<Integer> combine, int index) {
if (index == candidates.length) {
return;
}
if (target == 0) {
ans.add(new ArrayList<Integer>(combine));
return;
}
// 直接跳过
dfs(candidates, target, ans, combine, index + 1);
// 选择当前数
if (target - candidates[index] >= 0) {
combine.add(candidates[index]);
dfs(candidates, target - candidates[index], ans, combine, index);
combine.remove(combine.size() - 1);
}
}
}
n个骰子点数和
题目:把n个骰子扔在地上,所有骰子朝上一面的点数之和为s。输入n,打印出s的所有可能的值出现的概率。
public double[] dicesProbability(int n) {
//因为最后的结果只与前一个动态转移数组有关,所以这里只需要设置一个一维的动态转移数组
//原本dp[i][j]表示的是前i个骰子的点数之和为j的概率,现在只需要最后的状态的数组,所以就只用一个一维数组dp[j]表示n个骰子下每个结果的概率。
//初始是1个骰子情况下的点数之和情况,就只有6个结果,所以用dp的初始化的size是6个
double[] dp = new double[6];
//只有一个数组
Arrays.fill(dp,1.0/6.0);
//从第2个骰子开始,这里n表示n个骰子,先从第二个的情况算起,然后再逐步求3个、4个···n个的情况
//i表示当总共i个骰子时的结果
for(int i=2;i<=n;i++){
//每次的点数之和范围会有点变化,点数之和的值最大是i*6,最小是i*1,i之前的结果值是不会出现的;
//比如i=3个骰子时,最小就是3了,不可能是2和1,所以点数之和的值的个数是6*i-(i-1),化简:5*i+1
//当有i个骰子时的点数之和的值数组先假定是temp
double[] temp = new double[5*i+1];
//从i-1个骰子的点数之和的值数组入手,计算i个骰子的点数之和数组的值
//先拿i-1个骰子的点数之和数组的第j个值,它所影响的是i个骰子时的temp[j+k]的值
for(int j=0;j<dp.length;j++){
//比如只有1个骰子时,dp[1]是代表当骰子点数之和为2时的概率,它会对当有2个骰子时的点数之和为3、4、5、6、7、8产生影响,因为当有一个骰子的值为2时,另一个骰子的值可以为1~6,产生的点数之和相应的就是3~8;比如dp[2]代表点数之和为3,它会对有2个骰子时的点数之和为4、5、6、7、8、9产生影响;所以k在这里就是对应着第i个骰子出现时可能出现六种情况,这里可能画一个K神那样的动态规划逆推的图就好理解很多
for(int k=0;k<6;k++){
//这里记得是加上dp数组值与1/6的乘积,1/6是第i个骰子投出某个值的概率
temp[j+k]+=dp[j]*(1.0/6.0);
}
}
//i个骰子的点数之和全都算出来后,要将temp数组移交给dp数组,dp数组就会代表i个骰子时的可能出现的点数之和的概率;用于计算i+1个骰子时的点数之和的概率
dp = temp;
}
return dp;
}
最大子序和(连续子数组的最大和)
题目: 给定一个整数数组 nums ,找到一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。
输入:nums = [-2,1,-3,4,-1,2,1,-5,4]
输出:6
解释:连续子数组 [4,-1,2,1] 的和最大,为 6 。
class Solution {
public int maxSubArray(int[] nums) {
int tmpSum = 0, res = nums[0];
for(int num : nums){
tmpSum = Math.max(tmpSum + num, num);
res = Math.max(res, tmpSum);
}
return res;
}
}
时间复杂度:O(N)
空间复杂度:O(1)
数组中的逆序对(归并排序)
- 归并排序
//归并排序:分治法。将序列分为两半,分别进行归并排序,再合并
void merge(int[] arr, int start, int end) {
if (start == end) return;
int mid = (start + end) / 2;
merge(arr, start, mid);
merge(arr, mid + 1, end);
int[] temp = new int[end - start + 1];
int i = start, j = mid + 1, k = 0;
while(i <= mid && j <= end)
temp[k++] = arr[i] < arr[j] ? arr[i++] : arr[j++];
while(i <= mid)
temp[k++] = arr[i++];
while(j <= end)
temp[k++] = arr[j++];
System.arraycopy(temp, 0, arr, start, end);
}
- 此题
public int reversePairs(int[] nums) {
return merge(nums, 0, nums.length - 1);
}
int merge(int[] arr, int start, int end) {
if (start == end) return 0;
int mid = (start + end) / 2;
int count = merge(arr, start, mid) + merge(arr, mid + 1, end);
int[] temp = new int[end - start + 1];
int i = start, j = mid + 1, k = 0;
while (i <= mid && j <= end) {
count += arr[i] <= arr[j] ? j - (mid + 1) : 0;
temp[k++] = arr[i] <= arr[j] ? arr[i++] : arr[j++];
}
while (i <= mid) {
count += j - (mid + 1);
temp[k++] = arr[i++];
}
while (j <= end)
temp[k++] = arr[j++];
System.arraycopy(temp, 0, arr, start, end - start + 1);
return count;
}
数组中第一个比它大的元素距离
题目:例如,给定一个列表 temperatures = [73, 74, 75, 71, 69, 72, 76, 73],你的输出应该是 [1, 1, 4, 2, 1, 1, 0, 0]。
思想:单调栈。数组从左向右遍历,依次将元素下标压栈
压栈时需要与栈顶元素相比较,如果当前元素比栈顶元素大,则弹出栈顶,相减,即当前元素的距离;否则压栈。
class Solution {
public int[] dailyTemperatures(int[] T) {
Stack<Integer> stack = new Stack<>();
int n = T.length;
int[] index = new int[n];
for(int i = 0;i < n;i++){
while(!stack.isEmpty() && T[stack.peek()] < T[i]){
int res = stack.pop();
index[res] = i - res;
}
stack.push(i);
}
return index;
}
}
圆圈中最后剩下的数字(约瑟夫环)
题目: 总共是n个数字,每次删除第m个数字
思路:
最后只剩下一个元素,假设这个最后存活的元素为 num, 这个元素最终的的下标一定是0 (因为最后只剩这一个元素),所以如果我们可以推出上一轮次中这个num的下标,然后根据上一轮num的下标推断出上上一轮num的下标,直到推断出元素个数为n的那一轮num的下标,那我们就可以根据这个下标获取到最终的元素了。推断过程如下:
- 首先最后一轮中num的下标一定是0, 这个是已知的。
- 那上一轮应该是有两个元素,此轮次中 num 的下标为 (0 + m)%n = (0+3)%2 = 1; 说明这一轮删除之前num的下标为1;
- 再上一轮应该有3个元素,此轮次中 num 的下标为 (1+3)%3 = 1;说明这一轮某元素被删除之前num的下标为1;
- 再上一轮应该有4个元素,此轮次中 num 的下标为 (1+3)%4 = 0;说明这一轮某元素被删除之前num的下标为0;
- 再上一轮应该有5个元素,此轮次中 num 的下标为 (0+3)%5 = 3; 说明这一轮某元素被删除之前num的下标为3; 因为我们要删除的序列为0-n-1, 所以求得下标其实就是求得了最终的结果。
- 比如当n为5的时候,num的初始下标为3, 所以num就是3,也就是说从0-n-1的序列中,
- 经过n-1轮的淘汰,3这个元素最终存活下来了,也是最终的结果。 总结一下推导公式:
(此轮过后的num下标 + m) % 上轮元素个数 = 上轮num的下标
class Solution {
public int lastRemaining(int n, int m) {
int x = 0; //元素初始的下标
for (int i = 2; i <= n; i++) {
//倒数第二轮是2个元素
x = (x + m) % i;
}
return x;
}
}
7. 数组中的数的次数
多数元素(摩尔投票)
题目:给定一个大小为 n 的数组,找到其中的多数元素。多数元素是指在数组中出现次数 大于 ⌊ n/2 ⌋ 的元素。
思路:摩尔投票法
public int majorityElement(int[] num) {
int major = num[0];
int count = 1;
for (int i = 1; i < num.length; i++) {
if (count == 0) {
//前面都消完了,在重新赋值
count++;
major = num[i];
} else if (major == num[i]) {
//自己人,count就加1
count++;
} else {
//不是自己人就同归于尽,消掉一个
count--;
}
}
return major;
}
只出现一次的数字
问题:给定一个非空整数数组,除了某个元素只出现一次以外,其余每个元素均出现两次。找出那个只出现了一次的元素。
思路:数组中的全部元素的异或运算结果即为数组中只出现一次的数字。
class Solution {
public int singleNumber(int[] nums) {
int ans = 0;
for (int num : nums) {
ans ^= num;
}
return ans;
}
}
0-n~1中缺失的数字
题目:一个长度为n-1的递增排序数组中的所有数字都是唯一的,并且每个数字都在范围0~n-1之内。在范围0~n-1内的n个数字中有且只有一个数字不在该数组中,请找出这个数字。
思路:二分法
class Solution {
public int missingNumber(int[] nums) {
int i = 0, j = nums.length - 1;
while(i <= j) {
int m = (i + j) / 2;
if(nums[m] == m) i = m + 1;
else j = m - 1;
}
return i;
}
}
8. 场景
买卖股票的最佳时机
输入:[7,1,5,3,6,4]
输出:5
解释:在第 2 天(股票价格 = 1)的时候买入,在第 5 天(股票价格 = 6)的时候卖出,最大利润 = 6-1 = 5 。
注意利润不能是 7-1 = 6, 因为卖出价格需要大于买入价格;同时,你不能在买入前卖出股票。
思想:动态规划
public class Solution {
public 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;
}
}
买卖股票的最佳时期(多次买卖)
题目:输入: prices = [7,1,5,3,6,4]
输出: 7
解释: 在第 2 天(股票价格 = 1)的时候买入,在第 3 天(股票价格 = 5)的时候卖出, 这笔交易所能获得利润 = 5-1 = 4 。
随后,在第 4 天(股票价格 = 3)的时候买入,在第 5 天(股票价格 = 6)的时候卖出, 这笔交易所能获得利润 = 6-3 = 3 。
- 思想:对于每一天我们都有三种选择:
买入股票; 什么也不做; 卖出股票。
可得状态转化方程:
buy = max(buy, sell – p)
sell = max(sell, buy + p)- 第一天的买入收益即为第一天股票价格的相反数(买入价格为 p 的股票,花费 p ,收益为 -p)
buy = -prices[0]; 第一天卖出一定是在第一天买入的基础上进行的,同一天内股票价格相同,收益为0 sell = 0 ;
class Solution {
public int maxProfit(int[] prices) {
int buy = -prices[0], sell = 0;
for(int p: prices){
/*每一天都可以选择买或者卖*/
//若不买,则当前收益延续上次买入后的状态值;若买,则由上一次卖出股票后的当前总收益减去当日买入股票的花费转换而来;
buy = Math.max(buy, sell - p);
//若不卖,则当前收益延续上次卖出后的状态值;若卖,则由上一次买入股票后的当前总收益加上当日卖出股票的收益转换而来。
sell = Math.max(sell, buy + p);
}
return sell;
}
}
颜色分类
题目: 给定一个包含红色、白色和蓝色,一共 n 个元素的数组,原地对它们进行排序,使得相同颜色的元素相邻,并按照红色、白色、蓝色顺序排列。此题中,我们使用整数 0、 1 和 2 分别表示红色、白色和蓝色。
输入:nums = [2,0,2,1,1,0]
输出:[0,0,1,1,2,2]
思路:双指针 :left 指针指向数组的开始;right 指针指向数组的结尾。 若 index 位置上的元素值为 0,则说明是红色,要放在最前面,即和 left 指针指向位置上的元素进行交换;
若 index 位置上的元素值为1,则说明是白色(本来就是要放在中间)不需要进行交换,直接将 index 指针后移; 若 index 位置上的元素值为2,则说明是蓝色,要放在最后面,即和 right 指针指向位置上的元素进行交换。
class Solution {
public void sortColors(int[] nums) {
int n = nums.length;
int left = 0, right = n - 1;
for (int i = 0; i <= right; ++i) {
while (i <= right && nums[i] == 2) {
//遇到2交换到最后
int temp = nums[i];
nums[i] = nums[right];
nums[right] = temp;
--right;
}
if (nums[i] == 0) {
int temp = nums[i];
nums[i] = nums[left];
nums[left] = temp;
++left;
}
}
}
}
时间复杂度:O(n),其中 n 是数组 nums 的长度。
空间复杂度:O(1)。
爬楼梯(青蛙跳台阶)
题目:假设你正在爬楼梯。需要 n 阶你才能到达楼顶。
每次你可以爬 1 或 2 个台阶。你有多少种不同的方法可以爬到楼顶呢?
class Solution {
public int climbStairs(int n) {
int[] dp = new int[n + 1];
dp[0] = 1;
dp[1] = 1;
for(int i = 2; i <= n; i++) {
dp[i] = dp[i - 1] + dp[i - 2];
}
return dp[n];
}
}
跳跃游戏
题目:给定一个非负整数数组 nums ,你最初位于数组的 第一个下标 。
数组中的每个元素代表你在该位置可以跳跃的最大长度。
判断你是否能够到达最后一个下标。
public class Solution {
public boolean canJump(int[] nums) {
int n = nums.length;
int rightmost = 0;
for (int i = 0; i < n; ++i) {
if (i <= rightmost) {
rightmost = Math.max(rightmost, i + nums[i]);
if (rightmost >= n - 1) {
return true;
}
}
}
return false;
}
}
时间复杂度:O(n)O(n),其中 nn 为数组的大小。只需要访问 nums 数组一遍,共 nn 个位置。
空间复杂度:O(1)O(1),不需要额外的空间开销。
扑克牌中的顺子
输入: [1,2,3,4,5]
输出: True
输入: [0,0,1,2,5]
输出: True
思路:注意数组中最大值和最小值(除0之外)相差要小于数组长度
public boolean isStraight(int[] nums) {
int max = 0;
int min = 14;
Set<Integer> set = new HashSet<>();
for (int i = 0; i < nums.length; i++) {
if (nums[i] == 0) {
continue;
}
if (set.contains(nums[i])) {
return false;
}
set.add(nums[i]);
max = Math.max(max, nums[i]);
min = Math.min(min, nums[i]);
}
return max - min < nums.length;
}
机器人的运动范围
地上有一个m行n列的方格,从坐标 [0,0] 到坐标 [m-1,n-1] 。一个机器人从坐标 [0, 0] 的格子开始移动,它每次可以向左、右、上、下移动一格(不能移动到方格外),也不能进入行坐标和列坐标的数位之和大于k的格子。例如,当k为18时,机器人能够进入方格 [35, 37] ,因为3+5+3+7=18。但它不能进入方格 [35, 38],因为3+5+3+8=19。请问该机器人能够到达多少个格子?
//统计到达的数量
int counts=0;
public int movingCount(int m, int n, int k) {
//辅助数组 用来标记是否统计过
int[][] visited = new int[m][n];
//从 0,0位置开始统计
helper(visited,0,0,m-1,n-1,k);
return counts;
}
/** *传入i,j两点 判断当前点是否符合规则 符合规则下继续对下右两个方向递归判断 */
private void helper(int[][] visited,int i,int j,int m,int n,int k){
if(i<=m&&j<=n&&visited[i][j]!=1&&(indexSum(i)+indexSum(j))<=k){
counts++;
visited[i][j]=1;
helper(visited,i+1,j,m,n,k);
helper(visited,i,j+1,m,n,k);
}
}
/** *根据传入的数 求出各位上的数字累加和 */
private int indexSum(int index){
int sum = index%10;
int tmp = index/10;
while(tmp>0){
sum+=tmp%10;
tmp/=10;
}
return sum;
}
剪绳子
题目:给你一根长度为 n 的绳子,请把绳子剪成整数长度的 m 段(m、n都是整数,n>1并且m>1),每段绳子的长度记为 k[0],k[1]…k[m-1] 。请问 k[0]k[1]…*k[m-1] 可能的最大乘积是多少?例如,当绳子的长度是8时,我们把它剪成长度分别为2、3、3的三段,此时得到的最大乘积是18。
输入: 2
输出: 1
解释: 2 = 1 + 1, 1 × 1 = 1
public int cuttingRope(int n) {
int[] dp = new int[n + 1];//长度为i的绳子能得到的最大乘积
dp[1] = 1;
for (int i = 2; i <= n; i++) {
for (int j = 1; j < i; j++) {
dp[i] = Math.max(dp[i], (Math.max(j, dp[j])) * (Math.max(i - j, dp[i - j])));
}
}
return dp[n];
}
二进制中1的个数
思路:位运算。每次将n右移一位就可以更新得到新的最后一位,继续和1做与运算
如果n & 1 = 0, 则n的最后一位是0
如果n & 1 = 1, 则n的最后一位是1
public class Solution {
// you need to treat n as an unsigned value
public int hammingWeight(int n) {
// 返回结果
int res = 0;
while(n != 0){
res = res + (n & 1);
// 无符号右移1位
n = n >>> 1;
}
return res;
}
}
数值的整数次方
递归是比较好理解的
如果n == 0,返回1 如果n < 0,最终结果为 1/x^{-n}
如果n为奇数,最终结果为 x * x ^ {n – 1}
如果n为偶数,最终结果为 x ^ {2*(n/2)}
Java中因为n的最小值可以取到Integer.MIN_VALUE,如果直接取它的相反数的话还是它自己,会导致堆栈溢出,因此提一个x出来
class Solution {
public double myPow(double x, int n) {
if(n == 0){
return 1;
}else if(n < 0){
return 1 / (x * myPow(x, - n - 1));
}else if(n % 2 == 1){
return x * myPow(x, n - 1);
}else{
return myPow(x * x, n / 2);
}
}
}
二、链表
两数相加
题目:给你两个 非空 的链表,表示两个非负的整数。它们每位数字都是按照 逆序 的方式存储的,并且每个节点只能存储 一位 数字。请你将两个数相加,并以相同形式返回一个表示和的链表。
你可以假设除了数字 0 之外,这两个数都不会以 0 开头。
思路:同时遍历两个链表,逐位计算它们的和,并与当前位置的进位值相加。
如果两个链表的长度不同,则可以认为长度短的链表的后面有若干个 00 。
此外,如果链表遍历结束后,还有进位,还需要在答案链表的后面附加一个节点。
public class AddTwoNum {
public static class ListNode{
int val;
ListNode next;
ListNode(int x) {
val=x;}
}
public static ListNode addTwoNumbers(ListNode l1,ListNode l2){
{
ListNode head = null, tail = null;
int jinWei = 0; //进位
while (l1!=null || l2!=null ) {
int n1 = l1 != null ? l1.val : 0;
int n2 = l2 != null ? l2.val : 0;
int sum = n1 + n2 + jinWei;
if (head == null) {
head = tail = new ListNode(sum % 10);
} else {
tail.next = new ListNode(sum % 10);
tail = tail.next;
}
jinWei = sum / 10;
if (l1 != null) {
l1 = l1.next;
}
if (l2 != null) {
l2 = l2.next;
}
}
if (jinWei > 0) {
tail.next = new ListNode(jinWei);
}
return head;
}
}
public static void main(String[] args) {
ListNode a1 = new ListNode(2);
ListNode a2 = new ListNode(4);
ListNode a3 = new ListNode(3);
a1.next = a2;
a2.next = a3;
ListNode b1 = new ListNode(5);
ListNode b2 = new ListNode(6);
ListNode b3 = new ListNode(4);
b1.next = b2;
b2.next = b3;
ListNode resultNode = addTwoNumbers(a1, b1);
System.out.println(resultNode.val); //返回的是头结点
}
}
时间复杂度:O(max(m,n)),其中 m 和 n 分别为两个链表的长度。我们要遍历两个链表的全部位置,而处理每个位置只需要O(1) 的时间。
空间复杂度:O(1)。注意返回值不计入空间复杂度。
排序链表
题目:给你链表的头结点 head ,请将其按 升序 排列并返回排序后的链表 。
你可以在 O(n log n) 时间复杂度和常数级空间复杂度下,对链表进行排序吗?
/** * Definition for singly-linked list. * public class ListNode { * int val; * ListNode next; * ListNode() {} * ListNode(int val) { this.val = val; } * ListNode(int val, ListNode next) { this.val = val; this.next = next; } * } */
class Solution {
public ListNode sortList(ListNode head) {
if(head==null || head.next==null){
return head;
}
ListNode fast = head.next;
ListNode slow = head;
while(fast.next!=null && fast.next.next!=null){
fast = fast.next.next;
slow = slow.next;
}
ListNode tmp = slow.next;
slow.next = null; //切断操作
ListNode left = sortList(head);
ListNode right = sortList(tmp);
ListNode res = new ListNode();
ListNode ans = res;
while(left!=null && right!=null){
if(left.val<right.val){
res.next = left;
left = left.next;
}
else{
res.next = right;
right = right.next;
}
res = res.next;
}
res.next = left!=null ? left:right;
return ans.next;
}
}
从尾打印链表
//栈
/** * Definition for singly-linked list. * public class ListNode { * int val; * ListNode next; * ListNode(int x) { val = x; } * } */
class Solution {
public int[] reversePrint(ListNode head) {
LinkedList<Integer> stack = new LinkedList<>();
ListNode p = head;
while (p != null) {
stack.push(p.val);
p = p.next;
}
int[] res = new int[stack.size()];
int i = 0;
while (!stack.isEmpty()) {
res[i] = stack.pop();
i++;
}
return res;
}
}
输出链表中倒数第K个几点
题目:给定一个链表: 1->2->3->4->5, 和 k = 2.
返回链表 4->5.
思路:快慢指针
class Solution {
public ListNode getKthFromEnd(ListNode head, int k) {
ListNode fast = head, slow = head;
for(int i = 0; i < k; i++)
fast = fast.next;
while(former != null) {
fast = fast.next;
low = low.next;
}
return low;
}
}
删除链表的倒数第 N 个结点
思路:快慢指针,让前面的指针先移动n步,之后前后指针共同移动直到前面的指针到尾部为止
public class ListNode {
int val;
ListNode next;
ListNode(int x) {
val = x; }
}
public class Solution{
public ListNode removeNthFromEnd(ListNode head, int n) {
ListNode pre = new ListNode(0);
pre.next = head;
ListNode fast = pre;
ListNode slow = pre;
while(n != 0) {
fast = fast.next;
n--;
}
while(fast.next != null) {
fast = fast.next;
slow = slow.next;
}
slow.next = slow.next.next;
return pre.next;
}
}
时间复杂度:O(n)
合并两个有序链表
public ListNode mergeTwoLists(ListNode l1, ListNode l2) {
ListNode prehead = new ListNode(-1);
ListNode prev = prehead;
while (l1 != null && l2 != null) {
if (l1.val <= l2.val) {
prev.next = l1;
l1 = l1.next;
} else {
prev.next = l2;
l2 = l2.next;
}
prev = prev.next;
}
// 合并后 l1 和 l2 最多只有一个还未被合并完,我们直接将链表末尾指向未合并完的链表即可
prev.next = l1 == null ? l2 : l1;
return prehead.next;
}
时间复杂度:O(n + m),其中 n 和 m 分别为两个链表的长度。因为每次循环迭代中,l1 和 l2 只有一个元素会被放进合并链表中, 因此 while 循环的次数不会超过两个链表的长度之和。所有其他操作的时间复杂度都是常数级别的,因此总的时间复杂度为 O(n+m)。
空间复杂度:O(1)。我们只需要常数的空间存放若干变量。
环形链表
题目:给定一个链表,判断链表中是否有环。
思路:快慢指针
/** * Definition for singly-linked list. * class ListNode { * int val; * ListNode next; * ListNode(int x) { * val = x; * next = null; * } * } */
public class Solution {
public boolean hasCycle(ListNode head) {
if (head == null) return false;
ListNode fast = head;
ListNode slow = head;
while (fast != null && fast.next != null) {
fast = fast.next.next;
slow = slow.next;
if (fast == slow) return true;
}
return false;
}
}
- LRU缓存机制
排序链表
题目:给你链表的头结点 head ,请将其按 升序 排列并返回 排序后的链表
思路:归并排序
利用归并的思想,递归地将当前链表分为两段,然后 merge。
分两段的方法是使用快慢指针,fast 一次走两步,slow 一次走一步。因为 fast 指针走的遍历的节点数是 slow 指针遍节点数的两倍,所以当 fast 指针遍历到链表末尾时,此时 slow 指针所在位置就是链表的中间位置,这样就将当前链表分成了两段。
merge 时,把两段头部节点值比较,定义一个 p 指针指向较小的节点,且记录第一个节点,然后两段链表从头一步一步向后走,p 也一直向后走,总是指向较小节点,直至其中一个头为 NULL,继续处理剩下的元素,最后返回记录的头即可。
/** * Definition for singly-linked list. * class ListNode { * int val; * ListNode next; * ListNode(int x) { * val = x; * next = null; * } * } */
class Solution {
public ListNode sortList(ListNode head) {
if (head == null) return null;
return mergeSort(head);
}
private ListNode mergeSort(ListNode head) {
if (head == null || head.next == null) return head;
//利用快慢指针来找到链表的中点
ListNode fast = head;
ListNode slow = head;
while (fast != null && fast.next != null && fast.next.next != null) {
fast = fast.next.next;
slow = slow.next;
}
ListNode right = mergeSort(slow.next);
slow.next = null;
ListNode left = mergeSort(head);
return merge(left, right);
}
private ListNode merge(ListNode left, ListNode right) {
ListNode dummyHead = new ListNode(0);
ListNode p = dummyHead;
while (left != null && right != null) {
if (left.val <= right.val) {
p.next = left;
left = left.next;
} else {
p.next = right;
right = right.next;
}
p = p.next;
}
if (left != null) p.next = left;
if (right != null) p.next = right;
return dummyHead.next;
}
}
相交链表(两个链表的第一个公共节点)
题目:找到两个单链表相交的起始节点。
public class Solution {
public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
ListNode A = headA, B = headB;
while (A != B) {
A = A != null ? A.next : headB;
B = B != null ? B.next : headA;
}
return A;
}
}
反转链表
class Solution {
public ListNode reverseList(ListNode head) {
// 待输出链表的头结点
ListNode resHead = new ListNode();
// 遍历所给链表用的指针 curNode
ListNode curNode = head;
while(curNode != null) {
// 保存当前遍历的结点同时当前指针后移一位
ListNode temp = curNode;
curNode = curNode.next;
// 将当前遍历到的结点放入新链表头部之后
// insert temp node into result after NEW LIST'S HEAD
temp.next = resHead.next;
resHead.next = temp;
}
return resHead.next;
}
}
回文链表
题目:请判断一个链表是否为回文链表。
思路:利用栈
public boolean isPalindrome(ListNode head) {
//定义一个栈
Stack<ListNode> stack = new Stack<>();
ListNode node = head;
//挨个遍历链表的节点,并依次压入栈中
while (node != null) {
stack.push(node);
node = node.next;
}
while (!stack.isEmpty()) {
//head不断从头开始遍历,stack不断从尾开始遍历,直到全部遍历结束,如果过程中有不相等的情况,则不是回文链表
if (head.val == stack.pop().val) {
head = head.next;
} else {
return false;
}
}
return true;
}
三、栈
有效的括号
思路:栈
import java.util.Stack;
public boolean isValid(String s) {
Stack<Character> stack = new Stack<>();
if(s.isEmpty()) return true;
char[] chars = s.toCharArray();
//遍历所有的元素
for (char c : chars) {
//如果是左括号,就把他们对应的右括号压栈
if (c == '(') {
stack.push(')');
} else if (c == '{') {
stack.push('}');
} else if (c == '[') {
stack.push(']');
} else if (stack.isEmpty() || stack.pop() != c) {
//否则就只能是右括号。
//1,如果栈为空,说明括号无法匹配。
//2,如果栈不为空,栈顶元素就要出栈,和这个右括号比较。
//如果栈顶元素不等于这个右括号,说明无法匹配,
//直接返回false。
return false;
}
}
//最后如果栈为空,说明完全匹配,是有效的括号。
//否则不完全匹配,就不是有效的括号
return stack.isEmpty();
}
两个栈实现队列
思想:push操作就是往stack1中push,
pop操作需要分类:如果stack2为空,那么需要将stack1中的数据转移到stack2中,然后对stack2进行pop,如果stack2不为空,直接pop
import java.util.Stack;
public class Solution {
Stack<Integer> stack1 = new Stack<Integer>();
Stack<Integer> stack2 = new Stack<Integer>();
public void push(int node) {
stack1.push(node);
}
public int pop() {
if (stack2.size() <= 0) {
while (stack1.size() != 0) {
stack2.push(stack1.pop());
}
}
return stack2.pop();
}
}
push时间复杂度
pop空间复杂度
最小栈 ×
题目:设计一个支持 push ,pop ,top 操作,并能在常数时间内检索到最小元素的栈。
push(x) —— 将元素 x 推入栈中。
pop() —— 删除栈顶的元素。
top() —— 获取栈顶元素。
getMin() —— 检索栈中的最小元素。
class MinStack {
Deque<Integer> xStack;
Deque<Integer> minStack;
public MinStack() {
xStack = new LinkedList<Integer>();
minStack = new LinkedList<Integer>();
minStack.push(Integer.MAX_VALUE);
}
public void push(int x) {
xStack.push(x);
minStack.push(Math.min(minStack.peek(), x));
}
public void pop() {
xStack.pop();
minStack.pop();
}
public int top() {
return xStack.peek();
}
public int getMin() {
return minStack.peek();
}
}
四、二叉树
二叉树的前序遍历(非递归)
import java.util.*;
public class TreeNode{
public int val;
TreeNode left = null;
TreeNode right = null;
}
public static void preOrder(TreeNode head) {
if (head == null) {
return;
}
Stack<TreeNode> stack = new Stack<>();
stack.push(head);
while (!stack.isEmpty()) {
TreeNode node = stack.pop();
System.out.print(node.val + " ");
if (node.right != null) {
stack.push(node.right);
}
if (node.left != null) {
stack.push(node.left);
}
}
}
二叉树的中序遍历(非递归)
public static void inOrder(TreeNode head) {
if (head == null) {
return;
}
TreeNode cur = head;
Stack<TreeNode> stack = new Stack<>();
while (!stack.isEmpty() || cur != null) {
while (cur != null) {
stack.push(cur);
cur = cur.left;
}
TreeNode node = stack.pop();
System.out.print(node.value + " ");
if (node.right != null) {
cur = node.right;
}
}
}
二叉树的后序遍历(非递归)
前序遍历的过程 是 中左右。
将其转化成 中右左。也就是压栈的过程中优先压入左子树,在压入右子树。
然后将这个结果返回来,这里是利用栈的先进后出倒序打印。
public static void postOrder(TreeNode head) {
if (head == null) {
return;
}
Stack<TreeNode> stack1 = new Stack<>();
Stack<TreeNode> stack2 = new Stack<>();
stack1.push(head);
while (!stack1.isEmpty()) {
TreeNode node = stack1.pop();
stack2.push(node);
if (node.left != null) {
stack1.push(node.left);
}
if (node.right != null) {
stack1.push(node.right);
}
}
while (!stack2.isEmpty()) {
System.out.print(stack2.pop().value + " ");
}
}
二叉树的层序遍历(队列)
有两种类型
队列操作:
add()和remove()方法在失败时会抛出异常(不推荐)
queue.offer():添加元素
queue.poll():返回第一个元素,并在队列中删除
queue.peek():返回第一个元素
queue.size():队列长度
//类型一
[
[3],
[9,20],
[15,7]
]
public List<List<Integer>> levelOrder(TreeNode root) {
List<List<Integer>> res = new ArrayList<>();
Queue<TreeNode> queue = new LinkedList<>();
if (root != null) {
queue.add(root);
}
while (!queue.isEmpty()) {
int n = queue.size();
List<Integer> level = new ArrayList<>();
for (int i = 0; i < n; i++) {
TreeNode node = queue.poll();
level.add(node.val);
if (node.left != null) {
queue.add(node.left);
}
if (node.right != null) {
queue.add(node.right);
}
}
res.add(level);
}
return res;
}
//类型二[3,9,20,15,7]
class Solution {
public int[] levelOrder(TreeNode root) {
if(root == null) return new int[0];
Queue<TreeNode> queue = new LinkedList<>();
if (root != null) {
queue.add(root);
}
ArrayList<Integer> ans = new ArrayList<>();
while(!queue.isEmpty()) {
TreeNode node = queue.poll();
ans.add(node.val);
if(node.left != null) queue.add(node.left);
if(node.right != null) queue.add(node.right);
}
int[] res = new int[ans.size()];
for(int i = 0; i < ans.size(); i++)
res[i] = ans.get(i);
return res;
}
}
之字形打印二叉树
给定二叉树: [3,9,20,null,null,15,7],
3
/
9 20
/
15 7
返回其层次遍历结果:
[
[3],
[20,9],
[15,7]
]
class Solution {
public List<List<Integer>> levelOrder(TreeNode root) {
Queue<TreeNode> queue = new LinkedList<>();
List<List<Integer>> res = new ArrayList<>();
if(root != null) queue.add(root);
while(!queue.isEmpty()) {
List<Integer> tmp = new ArrayList<>();
for(int i = queue.size(); i > 0; i--) {
TreeNode node = queue.poll();
tmp.add(node.val);
if(node.left != null) queue.add(node.left);
if(node.right != null) queue.add(node.right);
}
if(res.size() % 2 == 1) Collections.reverse(tmp);
res.add(tmp);
}
return res;
}
}
二叉搜索树
二叉搜索树的第K大节点
中序遍历是递增序列
利用倒序的中序遍历
/** * Definition for a binary tree node. * public class TreeNode { * int val; * TreeNode left; * TreeNode right; * TreeNode(int x) { val = x; } * } */
class Solution {
int result;
int count;
public int kthLargest(TreeNode root, int k) {
dfs(root,k);
return result;
}
void dfs(TreeNode root,int k){
if(root==null) return;
dfs(root.right,k);
count++;
if(count==k){
result = root.val;
return;
}
dfs(root.left,k);
}
}
验证二叉搜索树
假设一个二叉搜索树具有如下特征:
节点的左子树只包含小于当前节点的数。
节点的右子树只包含大于当前节点的数。
所有左子树和右子树自身必须也是二叉搜索树。
public boolean isValidBST(TreeNode root) {
if (root == null)
return true;
Stack<TreeNode> stack = new Stack<>();
TreeNode pre = null;
while (root != null || !stack.isEmpty()) {
while (root != null) {
stack.push(root);
root = root.left;
}
root = stack.pop();
if (pre != null && root.val <= pre.val)
return false;
//保存前一个访问的结点
pre = root;
root = root.right;
}
return true;
}
时间复杂度 : O(n)O(n),其中 nn 为二叉树的节点个数。二叉树的每个节点最多被访问一次,因此时间复杂度为 O(n)O(n)。
空间复杂度 : O(n)O(n),其中 nn 为二叉树的节点个数。栈最多存储 nn 个节点,因此需要额外的 O(n)O(n) 的空间。
对称二叉树
题目:判断是否为对称二叉树
class Solution {
public boolean isSymmetric(TreeNode root) {
if(root==null || (root.left==null && root.right==null)) {
return true;
}
//用队列保存节点
LinkedList<TreeNode> queue = new LinkedList<TreeNode>();
//将根节点的左右孩子放到队列中
queue.add(root.left);
queue.add(root.right);
while(queue.size()>0) {
//从队列中取出两个节点,再比较这两个节点
TreeNode left = queue.removeFirst();
TreeNode right = queue.removeFirst();
//如果两个节点都为空就继续循环,两者有一个为空就返回false
if(left==null && right==null) {
continue;
}
if(left==null || right==null) {
return false;
}
if(left.val!=right.val) {
return false;
}
//将左节点的左孩子, 右节点的右孩子放入队列
queue.add(left.left);
queue.add(right.right);
//将左节点的右孩子,右节点的左孩子放入队列
queue.add(left.right);
queue.add(right.left);
}
return true;
}
}
二叉树的最大深度
//DFS 递归
class Solution {
public int maxDepth(TreeNode root) {
if(root == null){
return 0;
}
int l = maxDepth(root.left);
int r = maxDepth(root.right);
return Math.max(l, r) + 1;
}
}
时间复杂度:O(n)
空间复杂度:O(height),树的高度
//BFS
class Solution {
public int maxDepth(TreeNode root) {
if(root == null){
return 0;
}
Queue<TreeNode> queue = new LinkedList<>();
queue.offer(root);
int depth = 0;
while(!queue.isEmpty()){
int n = queue.size();
for(int i = 0; i < n; i++){
TreeNode node = queue.poll();
if(node.left != null){
queue.offer(node.left);
}
if(node.right != null){
queue.offer(node.right);
}
}
depth++;
}
return depth;
}
}
时间复杂度:O(n)
空间复杂度:O(width),树的宽度,最坏情况O(n)
从前序和中序遍历序列构造二叉树
class Solution {
public TreeNode buildTree(int[] preorder, int[] inorder) {
if(preorder.length == 0 || inorder.length == 0){
return null;
}
TreeNode tree = new TreeNode(preorder[0]);
for(int i=0;i<preorder.length;i++){
if(preorder[0] == inorder[i]){
tree.left = buildTree(Arrays.copyOfRange(preorder,1,i+1),Arrays.copyOfRange(inorder,0,i));
tree.right = buildTree(Arrays.copyOfRange(preorder,i+1,preorder.length),Arrays.copyOfRange(inorder,i+1,inorder.length));
}
}
return tree;
}
}
- 二叉树展开为链表
- 二叉树中的最大路径和
- 单词拆分
- 打家劫舍
- 岛屿数量
- 课程表
- 实现前缀树
- 最大正方形
翻转二叉树
用队列 BFS
public TreeNode invertTree(TreeNode root) {
if (root == null)
return root;
Queue<TreeNode> queue = new LinkedList<>();
queue.add(root);//相当于把数据加入到队列尾部
while (!queue.isEmpty()) {
//poll方法相当于移除队列头部的元素
TreeNode node = queue.poll();
//先交换子节点
TreeNode left = node.left;
node.left = node.right;
node.right = left;
if (node.left != null)
queue.add(node.left);
if (node.right != null)
queue.add(node.right);
}
return root;
}
二叉树的最近公共祖先
后序遍历
class Solution {
public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
if(root == null || root == p || root == q) return root;
TreeNode left = lowestCommonAncestor(root.left, p, q);
TreeNode right = lowestCommonAncestor(root.right, p, q);
if(left == null) return right;
if(right == null) return left;
return root;
}
}
判断平衡二叉树
/** * Definition for a binary tree node. * public class TreeNode { * int val; * TreeNode left; * TreeNode right; * TreeNode(int x) { val = x; } * } */
class Solution {
public boolean isBalanced(TreeNode root) {
if (root == null) {
return true;
}
return height(root) != -1;
}
private int height(TreeNode node) {
if (node == null) {
return 0;
}
int left = height(node.left);
if (left == -1) {
return -1;
}
int right = height(node.right);
// 如果左子树或者右子树返回的高度为-1.直接返回,不再继续向下递归
if (right == -1) {
return -1;
}
// 判断左右子树高度差
if (Math.abs(left - right) >= 2) {
return -1;
}
return Math.max(left, right) + 1;
}
}
N皇后
解题思路
经典回溯题
以行为单位,每一行放置一个Queen
【返回的条件】:每一行都走完了,说明找到了一个可行解,加入到结果列表中
【如何进行迭代】:遍历一行中所有的位置,判断每个位置的有效性,即向上寻找是否已经有Queen,和斜向是否有Queen(都是向上寻找即可,因为下面的还没有放),如果满足条件,则进行递归,继续找下一行。
class Solution {
List<List<String>> result = new ArrayList<>();
public List<List<String>> solveNQueens(int n) {
backtrack(new ArrayList<>(), 0, n);
return result;
}
public void backtrack(List<String> list, int row, int n) {
// 满足条件,加入到结果列表中,然后返回。
if(row == n) {
result.add(new ArrayList<>(list));
return;
}
for(int i = 0; i < n; i++) {
// 如果放置在该位置合法,则进行递归
if(isValid(i, list, row, n)) {
// 生成该行的字符串
list.add(generateString(i, n));
backtrack(list, row+1, n);
list.remove(list.size()-1);
}
}
}
public boolean isValid(int pos, List<String> list, int row, int n) {
// 判断竖着有没有重合的
for(int i = 0; i < row; i++) {
if(list.get(i).charAt(pos) == 'Q') {
return false;
}
}
// 判断斜向有没有重合的
for(int i = row-1, j = pos-1; i >= 0 && j >= 0; i--, j--) {
if(list.get(i).charAt(j) == 'Q') {
return false;
}
}
for(int i = row-1, j = pos+1; i >= 0 && j < n; i--, j++) {
if(list.get(i).charAt(j) == 'Q') {
return false;
}
}
return true;
}
public String generateString(int pos, int n) {
StringBuilder sb = new StringBuilder();
for(int i = 0; i < pos; i++) {
sb.append(".");
}
sb.append("Q");
for(int i = pos+1; i < n; i++) {
sb.append(".");
}
return sb.toString();
}
}
五、排序算法
快排
public class QuickSort {
public static void main(String[] args) {
int [] a = {
1,6,8,7,3,5,16,4,8,36,13,44};
quickSort(a,0,a.length-1);
for (int i = 0; i < a.length; i++) {
System.out.print(a[i]+" ");
}
}
private static void quickSort(int[] nums,int left,int right){
//定义递归出口
if (nums.length<0){
return;
}
if(left>=right){
return;
}
int i = left;
int j = right;
int pivot = nums[left];
while(i<j){
while(i<j && nums[j]>=pivot){
j--;
}
nums[i]=nums[j];
while(i<j && nums[i]<=pivot){
i++;
}
nums[j]=nums[i];
}
nums[i]=pivot;
quickSort(nums,left,i-1);
quickSort(nums,i+1,right);
}
}
大根堆+排序
//建立大根堆
public class DuiSort {
//堆是完全二叉树
private static void heapify(int[] tree,int n,int i){
//最后写递归出口
if(i>=n){
return;
}
//n:树中的节点数 i:表示对哪个节点做heapify
int c1 = 2*i+1;//左孩子
int c2 = 2*i+2;//右孩子
int max = i;//先假定最大值的根节点
//比较三者大小
if(c1<n && tree[c1]>tree[max]){
//注意c1和c2必须是存在的,也就是说i是非叶子节点
max = c1;
}
if(c2<n && tree[c2]>tree[max]){
max = c2;
}
if(max!=i){
//也就是说存在孩子节点大于父节点,则需要进行交换
swap(tree,max,i);//只是将对应下标的数组值进行交换。max还指向的是孩子节点
heapify(tree,n,max);//继续对向下孩子节点进行递归heapify
}
}
private static void swap(int[] arr, int i, int j) {
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
private static void build_heap(int[] tree,int n){
int last_node = n-1;
int parent = (last_node-1)/2;
for (int i = parent; i >= 0 ; i--) {//从最后一个非叶子节点开始,从下至上,从右至左
heapify(tree,n,i);
}
}
private static void heap_sort(int[] tree,int n){
//堆排序
build_heap(tree,n);
for (int i = n-1; i >=0 ; i--) {
swap(tree,0,i);//交换根节点与最后一个节点,也就是把最大值取下来
build_heap(tree,i);//然后重新建堆, i可以代表当前这棵树的节点个数
}
}
public static void main(String[] args) {
int tree[] = {
5,9,6,3,8,7};
heap_sort(tree,6);
for (int i = 0; i < 6; i++) {
System.out.println(tree[i]);
}
}
}
归并排序
/** * 归并排序 * * @param array * @return */
public static int[] MergeSort(int[] array) {
if (array.length < 2) return array;
int mid = array.length / 2;
int[] left = Arrays.copyOfRange(array, 0, mid);
int[] right = Arrays.copyOfRange(array, mid, array.length);
return merge(MergeSort(left), MergeSort(right));
}
/** * 归并排序——将两段排序好的数组结合成一个排序数组 * * @param left * @param right * @return */
public static int[] merge(int[] left, int[] right) {
int[] result = new int[left.length + right.length];
for (int index = 0, i = 0, j = 0; index < result.length; index++) {
if (i >= left.length)
result[index] = right[j++];
else if (j >= right.length)
result[index] = left[i++];
else if (left[i] > right[j])
result[index] = right[j++];
else
result[index] = left[i++];
}
return result;
}
冒泡排序
/** * 冒泡排序 * * @param array * @return */
public static int[] bubbleSort(int[] array) {
if (array.length == 0)
return array;
for (int i = 0; i < array.length; i++)
for (int j = 0; j < array.length - 1 - i; j++)
if (array[j + 1] < array[j]) {
int temp = array[j + 1];
array[j + 1] = array[j];
array[j] = temp;
}
return array;
}
六、字符串
替换空格
输入:s = “We are happy.”
输出:“We%20are%20happy.”
class Solution {
public String replaceSpace(String s) {
StringBuilder res = new StringBuilder();
for(Character c : s.toCharArray())
{
if(c == ' ') res.append("%20");
else res.append(c);
}
return res.toString();
}
}
第一个只出现一次的字符
题目:s = “abaccdeff”
返回 “b”
s = “”
返回 ” “
class Solution {
public char firstUniqChar(String s) {
if(s.equals("")){
return ' ';
}
char[] arr =s.toCharArray();
HashMap<Character,Integer>map = new HashMap<>();
for(int i = 0;i < arr.length;i++){
map.put(arr[i],map.getOrDefault(arr[i],0)+1);
}
int k = 0;
for( k = 0;k < arr.length;k++){
if(map.get(arr[k]) == 1){
break;
}
}
if(k >= arr.length){
return ' ';
}else{
return arr[k];
}
}
}
无重复字符的最长子串(滑动窗口)
题目:给定一个字符串,请你找出其中不含有重复字符的 最长子串 的长度。
思路:滑动窗口
import java.util.HashMap;
public class LengthOfLongestSubstring {
public static int lengthOfLongestSubstring(String s) {
if (s.length()==0) return 0;
HashMap<Character, Integer> map = new HashMap<Character, Integer>();
int max = 0;
int left = 0;
for(int i = 0; i < s.length(); i ++){
if(map.containsKey(s.charAt(i))){
left = Math.max(left,map.get(s.charAt(i)) + 1);
}
map.put(s.charAt(i),i);
max = Math.max(max,i-left+1);
}
return max;
}
public static void main(String[] args) {
System.out.println(lengthOfLongestSubstring("abcabcbb"));
}
}
时间复杂度:O(n)
最长回文子串
思路:中心扩散法
首先往左寻找与当期位置相同的字符,直到遇到不相等为止。
然后往右寻找与当期位置相同的字符,直到遇到不相等为止。
最后左右双向扩散,直到左和右不相等。
public class LongestHuiWen {
public static void main(String[] args) {
System.out.println(longestPalindrome1("babad"));
}
public static String longestPalindrome1(String s) {
if (s.length() == 0) {
return "";
}
int left = 0;
int right = 0;
int len = 1;
int maxStart = 0;//用来记录子串的起始位置
int maxLen = 0;
for (int i = 0; i < s.length(); i++) {
left = i - 1;
right = i + 1;
while (left >= 0 && s.charAt(left) == s.charAt(i)) {
//向左扩散
len++;
left--;
}
while (right < s.length() && s.charAt(right) == s.charAt(i)) {
//向右扩散
len++;
right++;
}
while (left >= 0 && right < s.length() && s.charAt(right) == s.charAt(left)) {
//两边扩散
len = len + 2;
left--;
right++;
}
if (len > maxLen) {
maxLen = len;
maxStart = left;
}
len = 1;
}
return s.substring(maxStart + 1, maxStart + maxLen + 1);
}
}
时间复杂度:O(n^2),其中 n 是字符串的长度。长度为 1和 2的回文中心分别有 n 和 n-1个,每个回文中心最多会向外扩展 O(n)次。
空间复杂度:O(1)。
最长公共子串
题目:给定两个字符串str1和str2,输出两个字符串的最长公共子串
题目保证str1和str2的最长公共子串存在且唯一。
思路:滑动窗口/动态规划 start和end描述了一个窗口
从str1中按照窗口截取窗口子串,检查str2中是否包含,如果包含就扩展窗口
如果str2中不包含窗口子串,我们就移动窗口起始位置
sb是用来记录最大的窗口子串的,窗口最小为0个单位
public static String LCS2(String str1, String str2) {
if (str1 == null || str2 == null) return "-1";
int n1 = str1.length(), n2 = str2.length();
if (n1 == 0 || n2 == 0) return "-1";
int[][] dp = new int[n1 + 1][n2 + 1];
int maxLen = 0, index = 0;
for (int i = 0; i < n1; i++) {
for (int j = 0; j < n2; j++) {
if (str1.charAt(i) == str2.charAt(j)) {
if (j == 0 && i == 0){
dp[i][j]=1;
maxLen = dp[i][j];
index = i;
} else{
dp[i][j] = dp[i - 1][j - 1] + 1;
if(maxLen <= dp[i][j]){
maxLen = dp[i][j];
index = i;
}
}
}
}
}
return maxLen == 0 ? "-1" : str1.substring(index - maxLen+1, index+1);
}
字符串转换成整数
class Solution {
public int strToInt(String str) {
//空串,返回0
if(str.length() == 0) return 0;
//转换为char数组方便遍历
char[] s = str.toCharArray();
//下标指针
int i = 0;
//结果
long total = 0;
//过滤空格
while(i < s.length - 1 && s[i] == ' ') i++;
//有效首字符非法,返回0
if((s[i] - '0' < 0 || s[i] - '0' > 9) && (s[i] != '+' && s[i] != '-')) return 0;
//正负号标识
boolean negative = false;
if(s[i] == '-'){
i++;
negative = true;
}else if(s[i] == '+'){
i++;
}
//进位累加,溢出返回整数边界
while(i < s.length && s[i] - '0' >= 0 && s[i] - '0' <= 9){
total = total * 10 + s[i] - '0';
if(!negative && total > Integer.MAX_VALUE) return Integer.MAX_VALUE;
else if(negative && -total < Integer.MIN_VALUE) return Integer.MIN_VALUE;
i++;
}
//返回结果
return (int)(negative? -total: total);
}
}
输入: “4193 with words”
输出: 4193
解释: 转换截止于数字 ‘3’ ,因为它的下一个字符不为数字。
public int strToInt(String str) {
str = str.trim();//去掉前后的空格
//如果为空,直接返回0
if (str.length() == 0)
return 0;
int index = 0;//遍历字符串中字符的位置
int res = 0;//最终结果
int sign = 1;//符号,1是正数,-1是负数,默认为正数
int length = str.length();
//判断符号
if (str.charAt(index) == '-' || str.charAt(index) == '+'){
sign = str.charAt(i) == '+'?1:-1;
index++;
}
for (; index < length; ++index) {
//取出字符串中字符,然后转化为数字
int digit = str.charAt(index) - '0';
//按照题中的要求,读入下一个字符,直到到达下一个非数字字符或到达输入的结尾。
//字符串的其余部分将被忽略。如果读取了非数字,后面的都要忽略
if (digit < 0 || digit > 9)
break;
//越界处理
if (res > Integer.MAX_VALUE / 10 || (res == Integer.MAX_VALUE / 10 && digit > Integer.MAX_VALUE % 10))
return sign == 1 ? Integer.MAX_VALUE : Integer.MIN_VALUE;
else
res = res * 10 + digit;
}
return sign * res;
}
左旋转字符串
题目:把字符串前面的若干个字符转移到字符串的尾部
输入: s = “abcdefg”, k = 2
输出: “cdefgab”
class Solution {
public String reverseLeftWords(String s, int n) {
return s.substring(n, s.length()) + s.substring(0, n);
}
}
class Solution {
public String reverseLeftWords(String s, int n) {
String res = "";
for(int i = n; i < s.length(); i++)
res += s.charAt(i);
for(int i = 0; i < n; i++)
res += s.charAt(i);
return res;
}
}
字符串逆序
// 方法一:
for (int i = 0; i < a.length(); i++) {
one += a.substring(a.length() - 1 - i, a.length() - i);
}
// 方法二:
StringBuffer stringBuffer = new StringBuffer(a);
two = stringBuffer.reverse().toString();
四则运算
题目:解析一般数学算式,实现简单的带括号的加减乘除运算。
思想:使用两个栈,一个数字栈,一个符号栈
从左往右遍历表达式字符串
遇到数字,直接压入数字栈
遇到符号
遇到左括号,直接入符号栈
遇到右括号,”符号栈弹栈取栈顶符号b,数字栈弹栈取栈顶数字a1,数字栈弹栈取栈顶数字a2,计算a2 b a1 ,将结果压入数字栈”,重复引号步骤至取栈顶为左括号,将左括号弹出
遇到运算符,1)若该运算符的优先级大于栈顶元素的优先级,直接入符号栈。2)若小于,”符号栈弹栈取栈顶符号b,数字栈弹栈取栈顶数字a1,数字栈弹栈取栈顶数字a2,计算a2 b a1 ,将结果压入数字栈”,重复引号步骤至该运算符的优先级大于符号栈顶元素的优先级,然后将该符号入符号栈
遍历结束后,”符号栈弹栈取栈顶符号b,数字栈弹栈取栈顶数字a1,数字栈弹栈取栈顶数字a2,计算a2 b a1 ,将结果压入数字栈”,重复引号步骤至符号栈无符号(或数字栈只有一个元素),则数字栈的元素为运算结果
参考
发布者:全栈程序员-用户IM,转载请注明出处:https://javaforall.cn/100176.html原文链接:https://javaforall.cn
【正版授权,激活自己账号】: Jetbrains全家桶Ide使用,1年售后保障,每天仅需1毛
【官方授权 正版激活】: 官方授权 正版激活 支持Jetbrains家族下所有IDE 使用个人JB账号...