目录(序号为leetcode题号)
- 29.两数相除
- 33.搜索旋转排序数组
- 34.在排序数组中查找元素的第一个和最后一个位置
- 36.有效的数独
- 38.外观数列
- 41.缺失的第一个正数
- 42.接雨水
- 44.通配符匹配
- 46.全排列
- 48. 旋转图像
29.两数相除
给定两个整数,被除数 dividend 和除数 divisor。将两数相除,要求不使用乘法、除法和 mod 运算符。返回被除数 dividend 除以除数 divisor 得到的商。
整数除法结果应当截去(truncate)其小数部分,例如:truncate(8.34) = 8 以及 truncate(-2.73) = -2
提示:
- 被除数和除数均为 32 位有符号整数。
- 除数不为 0。
- 假设我们的环境只能存储 32 位有符号整数,其数值范围是 [−231, 231 − 1]。本题中,如果除法结果溢出,则返回 231 − 1。
解题思路:
分析可能溢出的情况:
当被除数为32位有符号整数的最小值 -231时:
如果除数为1,那么我们可以直接返回答案 -231;
如果除数为-1,那么答案为 231,产生了溢出。此时我们需要返回 2^{31} −1。
当除数为32位有符号整数的最小值 -2^{31} 时:
如果被除数同样为 -2^{31},那么我们可以直接返回答案 1;
对于其余的情况,我们返回答案0。
当被除数为0时,我们可以直接返回答案0。
对于正常的情况,用被除数减去除数,同时记录下商的值。
public int divide(int dividend, int divisor) {
int c = 0;
// 判断溢出的情况。
if(dividend == Integer.MIN_VALUE){
if(divisor == 1){
return Integer.MIN_VALUE;
}
if(divisor == -1){
return Integer.MAX_VALUE;
}
if(divisor == Integer.MIN_VALUE){
return 1;
}
// 被除数为最小值,为了避免求绝对值,出现溢出,故进行加1操作。
dividend = dividend + 1;
c = 1;
}
if(dividend == 0||divisor == Integer.MIN_VALUE){
return 0;
}
// f记录符号。
int f = (dividend<0?-1:1)*(divisor<0?-1:1);
// r记录商
int r = 0;
// 逼近被除数值。
int n = 1;
dividend = Math.abs(dividend);
divisor = Math.abs(divisor);
while(dividend >= divisor){
if(dividend >= n*divisor){
dividend-=n*divisor;
r=r+n;
n = n*2;
}else {
n = 1;
}
}
if(dividend + c >= divisor){
r++;
}
return r*f;
}
33.搜索旋转排序数组
整数数组 nums 按升序排列,数组中的值互不相同 。
在传递给函数之前,nums 在预先未知的某个下标 k(0 <= k < nums.length)上进行了 旋转,使数组变为 [nums[k], nums[k+1], …, nums[n-1], nums[0], nums[1], …, nums[k-1]](下标 从 0 开始 计数)。例如, [0,1,2,4,5,6,7] 在下标 3 处经旋转后可能变为 [4,5,6,7,0,1,2] 。
给你 旋转后 的数组 nums 和一个整数 target ,如果 nums 中存在这个目标值 target ,则返回它的下标,否则返回 -1 。
解题思路:二分查找,旋转的排序数组,二分后必要一边的数组是有序的,故可以判断目标值在那一边。然后进行新的二分查找。
public int search(int[] nums, int target) {
int left = 0;
int right = nums.length - 1;
int mid = 0;
while(left<=right){
mid = left + ((right-left)>>1);
if(target == nums[mid]){
return mid;
}
if(nums[mid]>=nums[left]){
// 左边有序
if(target>=nums[left] && target<nums[mid]){
right = mid -1;
}else{
left = mid + 1;
}
}else{
// 右边有序
if(target>nums[mid] && target<= nums[right]){
left = mid + 1;
}else{
right = mid - 1;
}
}
}
return -1;
}
34.在排序数组中查找元素的第一个和最后一个位置
给定一个按照升序排列的整数数组 nums,和一个目标值 target。找出给定目标值在数组中的开始位置和结束位置。
如果数组中不存在目标值 target,返回 [-1, -1]。
解题分析:二分查找,分两次查找,第一次找到最开始位置的目标值,第二次找到结束位置的目标值。
public int[] searchRange(int[] nums, int target) {
if(nums.length == 0){
return new int[]{-1,-1};
}
if(nums.length == 1){
if(target == nums[0]){
return new int[]{0,0};
}
return new int[]{-1,-1};
}
int l = 0,r = nums.length-1;
int ft,lt;
// 找到开始位置的目标值位置
ft = ftwoSearch(nums,target,l,r);
if(ft == -1){
return new int[]{-1,-1};
}
// 找到结束位置的目标值位置
lt = ltwoSearch(nums,target,l,r);
return new int[]{ft,lt};
}
// 开始位置目标值的二分查找
public int ftwoSearch(int[] nums, int target,int left,int right){
int mid;
while(left < right){
mid = left + ((right-left)>>1);
if(target < nums[mid]){
// 搜索区间 [left,mid-1]
right = mid - 1;
}else if(target == nums[mid]){
// 搜索区间 [left,mid],此时不一定是开始位置的目标值。继续二分查找
right = mid;
}else{
// target 大于mid,搜索区间 [mid+1,right]
left = mid + 1;
}
}
return nums[left] == target?left:-1;
}
// 结束位置目标值的二分查找
public int ltwoSearch(int[] nums, int target,int left,int right){
int mid;
while(left < right){
// mid值向上取整,保证得到最右边的值。
mid = left+((right + 1 - left)>>1);
if(target > nums[mid]){
// 搜索区间 [mid+1,right]
left = mid + 1;
}else if(target == nums[mid]){
// 搜索区间 [mid,right],此时不一定是结束位置的目标值。继续二分查找
left = mid;
}else{
// 搜索区间 [left,mid-1]
right = mid - 1;
}
}
return left;
}
36.有效的数独
请你判断一个9 x 9 的数独是否有效。只需要根据以下规则,验证已经填入的数字是否有效即可。
数字 1-9 在每一行只能出现一次。
数字 1-9 在每一列只能出现一次。
数字 1-9 在每一个以粗实线分隔的 3x3 宫内只能出现一次。
一个有效的数独(部分已被填充)不一定是可解的。
只需要根据以上规则,验证已经填入的数字是否有效即可。
空白格用 ‘.’ 表示。
解题思路:对数组进行遍历,对行,列,3*3块进行判断。
public boolean isValidSudoku(char[][] board) {
HashSet<Character>[] lineSet = new HashSet[9];
HashSet<Character>[] blockSet = new HashSet[3];
HashSet<Character> rowSet = new HashSet<>();
blockSet[0] = new HashSet<>();
blockSet[1] = new HashSet<>();
blockSet[2] = new HashSet<>();
for(int i =0;i<9;i++){
lineSet[i] = new HashSet<>();
}
char t;
for(int i=0;i<9;i++){
rowSet.clear();
if(i % 3 == 0){
blockSet[0].clear();
blockSet[1].clear();
blockSet[2].clear();
}
for(int j = 0;j<9;j++){
t = board[i][j];
if(t !='.'){
// row的处理
if(rowSet.contains(t)){
return false;
}
rowSet.add(t);
// block的处理
// j=0-2,0 j=3-5, 1 j=6-8, 2
if(blockSet[j/3].contains(t)){
return false;
}
blockSet[j/3].add(t);
// line的处理
if(lineSet[j].contains(t)){
return false;
}
lineSet[j].add(t);
}
}
}
return true;
}
38.外观数列
给定一个正整数 n ,输出外观数列的第 n 项。「外观数列」是一个整数序列,从数字 1 开始,序列中的每一项都是对前一项的描述。
1. 1
2. 11
3. 21
4. 1211
5. 111221
第一项是数字 1
描述前一项,这个数是 1 即 “ 一 个 1 ”,记作 "11"
描述前一项,这个数是 11 即 “ 二 个 1 ” ,记作 "21"
描述前一项,这个数是 21 即 “ 一 个 2 + 一 个 1 ” ,记作 "1211"
描述前一项,这个数是 1211 即 “ 一 个 1 + 一 个 2 + 二 个 1 ” ,记作 "111221"
解题思路:根据 第n项求n+1项,将第1到第8项存在表中,加快求解速度。
public String countAndSay(int n) {
//把前八个结果存入表中。
String[] ary = {"","1","11","21","1211","111221","312211","13112221","1113213211"};
if(n<=8){
return ary[n];
}
String result = ary[8];
StringBuffer t = new StringBuffer();
int i = 9;
int key,value;
while(i <= n){
key = result.charAt(0)-'0';
value= 1;
for(int k = 1; k < result.length();k++){
if(key == result.charAt(k)-'0'){
value++;
}else {
t.append(value);
t.append(key);
key = result.charAt(k)-'0';
value = 1;
}
}
t.append(value);
t.append(key);
result = t.toString();
t.delete(0,t.length());
i++;
}
return result;
}
41.缺失的第一个正数
给你一个未排序的整数数组 nums ,请你找出其中没有出现的最小的正整数。
请你实现时间复杂度为 O(n) 并且只使用常数级别额外空间的解决方案。
解题思路:核心思想就是将给定的整数数组作为哈希表,记录已经出现的正整数,然后在判断未出现的最小正整数。将元素放在正确的下标位置。
数组长度为 n,那么最小未出现的正整数一定在 [1,n+1]的区间
public int firstMissingPositive(int[] nums) {
// 数组长度为 n,那么最小未出现的正整数一定在 [1,n+1]的区间
// 将出现的正整数放在正确的位置上,例如:1放在下标0的位置,2放在下标1的位置
int len = nums.length;
int t;
for(int i = 0; i < len; i++){
// 完成元素的交换,将元素放在正确的下标位置
while((nums[i]>=1 && nums[i] <= len) && nums[nums[i]-1] != nums[i]){
// 交换
t = nums[nums[i]-1];
nums[nums[i]-1] = nums[i];
nums[i] = t;
}
}
// 找到第一个不在正确位置的元素,i+1就是缺失的元素
for(int i = 0; i<len;i++){
if(nums[i] != i+1){
return i+1;
}
}
return len+1;
}
42.接雨水
给定 n 个非负整数表示每个宽度为 1 的柱子的高度图,计算按此排列的柱子,下雨之后能接多少雨水。
示例:
输入:height = [0,1,0,2,1,0,1,3,2,1,2,1]
输出:6
解释:上面是由数组 [0,1,0,2,1,0,1,3,2,1,2,1] 表示的高度图,在这种情况下,
可以接 6 个单位的雨水(蓝色部分表示雨水)。
方法一:暴力解法
解题思路:遍历数组,求解当前位置上能存储水的值,当前位置能存储水的值取决于两边的最大高度中的最小高度。即 当前位置左边的最大高度leftMax,当前位置右边的最大高度rightMax。则当前位置能存储水的值为 min(leftMax,rightMax) - 当前位置的值。
public int trap(int[] height){
int nums = 0;
int i,k;
int leftMax;
int rightMax;
for(i = 1;i<height.length;i++){
leftMax = 0;
rightMax = 0;
for(k = i;k >= 0; k--){
leftMax = Math.max(leftMax, height[k]);
}
for(k = i;k<height.length;k++){
rightMax = Math.max(rightMax, height[k]);
}
nums += (Math.min(leftMax,rightMax) - height[i]);
}
return nums;
}
方法二:动态规划
解题思路:当前位置的左边的最大高度为当前位置的值与当前位置减一的位置的左边最大高度的值的最大值。即 leftMax[i] = max(height[i],leftMax[i-1])
同理当前位置右边的最大高度为 :rightMax[i] = max(height[i],rightMax[i+1])。
可以记录下前一个位置的最大值,求当前位置的最大值。
public int trap(int[] height){
int nums = 0;
int[] leftMax = new int[height.length];
int[] rightMax = new int[height.length];
leftMax[0] = height[0];
rightMax[height.length-1] = height[height.length - 1];
for(int i=1; i<height.length; i++){
leftMax[i] = Math.max(leftMax[i-1], height[i]);
}
for(int i=height.length - 2; i >= 0; i--){
rightMax[i] = Math.max(rightMax[i+1], height[i]);
}
for(int i = 1;i < height.length;i++){
nums += (Math.min(leftMax[i],rightMax[i]) - height[i]);
}
return nums;
}
方法三:栈的应用
解题思路:积水只能在低洼处形成,当后面的柱子高度比前面的低时,是无法接雨水的。所以使用单调递减栈储存可能储水的柱子,当找到一根比前面高的柱子,就可以计算接到的雨水。
栈顶元素为最大值。
洼地,即左右两边的值比当前值大。
public int trap(int[] height) {
Stack<Integer> stack = new Stack<>();
stack.push(0);
int top;
int nums = 0;
int distance,h;
for(int i = 1;i < height.length;i++) {
while(!stack.empty() && height[stack.peek()] < height[i]) {
top = stack.pop();
if(stack.empty()){
// 栈为空,即左边没有值,洼地无法形成,不能存水。
break;
}
// 求洼地中存水的面积,长 * 高
// distance为长
distance = i - stack.peek() - 1;
// h高,洼地左右两边的最小值减去洼地位置的值。
h = Math.min( height[i],height[stack.peek()]) - height[top];
nums += distance * h;
}
stack.push(i);
}
return nums;
}
方法四:双指针
解题思路:维护两个指针left,right,开始分别指向最左端,最右端。比较两个指针指向的值,如果左边的值小,left++,如果右边的值小right–。
这是因为对于一边的值小,则必有另一边的值大于等于小的值,就可以尝试构造洼地,洼地的高度依赖于两边的最小值。
public int trap(int[] height) {
int left = 0,right = height.length-1;
int leftMax = 0,rightMax = 0;
int nums = 0;
while(left < right){
if(height[left]<= height[right]){
// 左边的值小
// 判断当前位置是否为洼地,即左边的最大值大于当前值
if(leftMax > height[left]){
// 洼地左边的高度为左边的最大值
// 洼地右边一定有值大于等于左边的最大值
// 综上,洼地的存储水的值为左边的最大值 - 当前洼地的值
nums += leftMax-height[left];
}
leftMax = Math.max(height[left],leftMax);
left++;
}else{
// 右边的值小
if(rightMax > height[right]){
// 洼地的右边值为右边的最大值
// 洼地左边边一定有值大于等于右边的最大值
// 综上,洼地的存储水的值为右边的最大值 - 当前洼地的值
nums += rightMax-height[right];
}
rightMax = Math.max(height[right],rightMax);
right--;
}
}
return nums;
}
44.通配符匹配
给定一个字符串 (s) 和一个字符模式 ( p),实现一个支持 ‘?’ 和 ‘*’ 的通配符匹配。
两个字符串完全匹配才算匹配成功。
? 可以匹配任何单个字符。* 可以匹配任意字符串(包括空字符串)。
说明:
s 可能为空,且只包含从 a-z 的小写字母。
p 可能为空,且只包含从 a-z 的小写字母,以及字符 ? 和 *。
解题思路:动态规划,使用dp数组来记录字符串的匹配状态。dp[i][j]可以表示,字符串 s的 第i个字符与字符串 p的第j个字符之前的字符串是否匹配。dp[i][j]若为true,则表示匹配,false则不匹配。
下面根据不同的考虑 dp[i][j] 该如何取值?
若当前 p[j] 的值不为 *,若 p[j] = s[i],表示当前字符匹配,则 dp[i][j]的值取决于dp[i-1][j-1]的值。
若 p[j] != s[i],则当前dp[i][j]的取值为false。
若当前p[j] 的值为 *,因为 * 可以匹配空字符,可以匹配任何字符,则需要综合考虑。若考虑 p[j]去匹配当前的s[i],则相当于s[i]不存在、即s[i]的值不影响dp[i][j]的值,dp[i][j]的取值为 dp[i-1][j]。若不考虑p[j]去匹配字符s[i],则dp[i][j]的取值为dp[i][j-1]。
要综合考虑这两种情况,所以dp[i][j] = dp[i-1][j] || dp[i][j-1]
综上得到 dp[i][j]的状态转移方程:
不明白可以看着表思考
边界条件,考虑 s和p都为空的情况,dp[0][0]为true。
s不为空,p为空,则 dp[i][0] 都为false。
s为空,p不为空,则若 p[j]不为 * ,dp[0][j]为false,若p[j]为空,则 dp[0][j] = dp[0][j-1]
最后 dp[s.length()][p.length()] 就是表示s与p是否匹配的值。
public boolean isMatch(String s, String p) {
int slen = s.length(),plen = p.length();
boolean[][] dp = new boolean[slen+1][plen+1];
// 初始化边界条件
dp[0][0] = true;
for(int j = 0;i<plen;i++){
if(p.charAt(j) == '*'){
dp[0][j+1] = dp[0][j];
}else{
dp[0][j+1] = false;
}
}
// 使用状态转移方程求解
for(int i=0;i < slen;i++){
for(int j=0;j < plen;j++){
if(p.charAt(j)!='*'){
// 不等于 *
if(p.charAt(j) == s.charAt(i) || p.charAt(j) == '?'){
dp[i+1][j+1] = dp[i][j];
}else{
dp[i+1][j+1] = false;
}
}else{
// 等于 *
dp[i+1][j+1] = dp[i][j+1]||dp[i+1][j];
}
}
}
return dp[slen][plen];
}
46.全排列
给定一个不含重复数字的数组 nums ,返回其 所有可能的全排列 。
输入:nums = [1,2,3]
输出:[[1,2,3],[1,3,2],[2,1,3],[2,3,1],[3,1,2],[3,2,1]]
解题思路:回溯法。将所有的可能都求出来。
public List<List<Integer>> permute(int[] nums) {
List<List<Integer>> res = new ArrayList<>();
List<Integer> output = new ArrayList<Integer>();
for (int num : nums) {
output.add(num);
}
int n = nums.length;
// n为数组的长度,output是数组,res存放所有的结果,0是层数
backtrack(n, output, res,0);
return res;
}
public void backtrack(int n, List<Integer> output, List<List<Integer>> res, int first) {
// 所有数都填完了
if (first == n) {
res.add(new ArrayList(output));
}
for (int i = first; i < n; i++) {
// 动态维护数组
Collections.swap(output, first, i);
// 继续递归填下一个数
backtrack(n, output, res, first + 1);
// 撤销操作
Collections.swap(output, first, i);
}
}
48. 旋转图像
给定一个 n × n 的二维矩阵 matrix 表示一个图像。请你将图像顺时针旋转 90 度。
你必须在 原地 旋转图像,这意味着你需要直接修改输入的二维矩阵。请不要 使用另一个矩阵来旋转图像。
示例:
输入:matrix = [[1,2,3],[4,5,6],[7,8,9]]
输出:[[7,4,1],[8,5,2],[9,6,3]]
解题思路:矩阵从外层到内层依次旋转,层数为 n/2。对于每次旋转只需要关注四个数即可。
示例:下面为一个 4*4的矩阵。矩阵的层数为两层,则需要对外层的各个元素旋转一次,对内层的各个元素旋转一次。共需要旋转两次。
对于每次旋转,关注四个数即可,如下图中的四个数,5,11,16,15。
只需要依次将5旋转到11的位置,将11旋转到16的位置,16旋转到15的位置,15旋转到5的位置,即可完成一次旋转。
对于每层的旋转次数为 (n-1-i)。i 的值与层数相关,最外层为i = 0,每次进入下一内层 i + 2;
在程序中只需要用将四个数的位置找出来即可,进行旋转即可。
public void rotate(int[][] matrix) {
int n = matrix.length;
int t;
// i为循环层数的次数。
for(int i = 0; i < n/2; i++){
// 每层需要旋转的次数,内层比外层要减2次,i加1,j=i,正好控制每层依次减少2次
for(int j = i; j < n - 1 - i;j++){
/*
右上角的值为 [i][j]
左上角的值为 [j][n-i-1]
右下角的值为 [n-j-1][i]
左下角的值为 [n-i-1][n-j-1]
*/
t = matrix[i][j];
matrix[i][j] = matrix[n-j-1][i];
matrix[n-j-1][i] = matrix[n-i-1][n-j-1];
matrix[n-i-1][n-j-1] = matrix[j][n-i-1];
matrix[j][n-i-1] = t;
}
}
}