本次题目
JZ53-I 在排序数组中查找数字 I
- 先使用二分查找搜索目标数字位置,找到目标数字后再向两边计算目标数字出现的次数(优化:两次二分查找,分别找到左边界和右边界)
- 时间复杂度O(logn),空间复杂度O(1)
class Solution {
public int search(int[] nums, int target) {
//二分查找,左闭右闭
int left = 0;
int right = nums.length - 1;
int count = 0;
while(left <= right){
int mid = left + (right - left) / 2;
if(nums[mid] > target){
right = mid - 1;
}else if(nums[mid] < target){
left = mid + 1;
}else{
//向两边搜索
int i = mid;
int j = mid;
count += 1;
while(i >= 1 && nums[i - 1] == target){
count++;
i--;
}
while(j < nums.length - 1 && nums[j + 1] == target){
count++;
j++;
}
return count;
}
}
return count;
}
//优化
public int search(int[] nums, int target) {
//二分查找,左闭右闭
//搜索右边界
int left = 0;
int right = nums.length - 1;
int rightBorder = 0;
while(left <= right){
int mid = left + (right - left) / 2;
if(nums[mid] > target){
right = mid - 1;
}else if(nums[mid] <= target){
left = mid + 1;
}
}
if(left == 0 || nums[left - 1] != target) return 0; //未搜索到目标值
rightBorder = left;
//搜索左边界
int leftBorder = 0;
left = 0;
right = rightBorder; //左边界肯定在右边界左边
while(left <= right){
int mid = left + (right - left) / 2;
if(nums[mid] >= target){
right = mid - 1;
}else if(nums[mid] < target){
left = mid + 1;
}
}
leftBorder = right;
return rightBorder - leftBorder - 1;
}
}
JZ53-II 0~n-1中缺失的数字
- 二分查找,二分后得到的数需要和下标一致,如果大于下标则向左边搜索(左边缺一位),如果等于下标则向右边搜索
- 时间复杂度O(logn),空间复杂度O(1)
class Solution {
public int missingNumber(int[] nums) {
//二分查找
int left = 0;
int right = nums.length - 1;
while(left <= right){
int mid = left + (right - left) / 2;
if(nums[mid] > mid){
right = mid - 1;
}else{
left = mid + 1;
}
}
return left;
}
}
JZ54 二叉搜索树的第k大节点
- 中序遍历(二叉搜索树中序遍历递增),计数返回结果
- 时间复杂度O(n),空间复杂度O(n)
class Solution {
int res, count; //注意这里使用全局变量不容易混淆
public int kthLargest(TreeNode root, int k) {
//中序遍历的逆序右中左
count = 0;
reverseInorder(root, k);
return res;
}
private void reverseInorder(TreeNode root, int k){
if(root == null) return;
if(count == k) return;
reverseInorder(root.right, k);
count++;
if(count == k){
res = root.val;
return;
}
reverseInorder(root.left, k);
}
}
JZ55-I 二叉树的深度
- 前序遍历或后序遍历,到达叶子节点时计算当前深度
- 时间复杂度O(n),空间复杂度O(二叉树深度)
class Solution {
int res;
public int maxDepth(TreeNode root) {
//前序遍历
res = 0;
if(root == null) return res;
preorder(root, 1);
return res;
}
//前序遍历,输入根节点和当前深度
private void preorder(TreeNode root, int depth){
//到达叶子节点
if(root != null && root.left == null && root.right == null){
res = Math.max(res, depth);
return;
}
depth++;
if(root.left != null) preorder(root.left, depth);
if(root.right != null) preorder(root.right, depth);
}
}
//优化
class Solution {
public int maxDepth(TreeNode root) {
//后序遍历
if(root == null) return 0;
int left = maxDepth(root.left);
int right = maxDepth(root.right);
return Math.max(left, right) + 1;
}
}
JZ55-II 平衡二叉树
- 递归,后序遍历,先判断左右子树是否平衡进行剪枝,若左右子树平衡,再判断当前节点是否平衡
- 时间复杂度O(n),空间复杂度O(n)
class Solution {
public boolean isBalanced(TreeNode root) {
//递归
int res = getDepth(root);
if(res == -1) return false;
return true;
}
//递归函数
private int getDepth(TreeNode root){
if(root == null) return 0;
//后序遍历
//注意先判断左右子树是否平衡
int left = getDepth(root.left);
if(left == -1) return -1;
int right = getDepth(root.right);
if(right == -1) return -1;
//若左右子树都平衡,判断当前节点是否平衡
if(Math.abs(left - right) > 1) return -1;
return Math.max(left, right) + 1;
}
}
JZ56-I 数组中数字出现的次数
-
如果数组中只有一个数字出现一次,则可以通过遍历数组进行异或运算得到该数字;本题中为两个数字x,y出现一次,因此先遍历数组进行异或运算得到x异或y,然后通过该结果将数组分为两部分,两部分分别包含x和y,再分别对两数组进行遍历异或得到x和y
- 时间复杂度O(n),空间复杂度O(1)
class Solution {
public int[] singleNumbers(int[] nums) {
//遍历数组进行异或得到x⊕y
int res = 0;
for(int num : nums){
res ^= num;
}
//取res中为1的二进制位作为分类依据
int div = 1; //从第一位开始与运算
while((div & res) == 0){ //如果该位为0
div <<= 1; //左移一位(*2)
}
int x = 0, y = 0;
//遍历数组将数组分为两部分进行异或
for(int num : nums){
if((div & num) == 0){
x ^= num;
}else{
y ^= num;
}
}
return new int[]{x, y};
}
}
JZ56-II 数组中数字出现的次数II
- 遍历数组中各个数字的二进制位,统计每一位中1的出现次数并对3求余,结果为出现一次的数字
- 时间复杂度O(n),空间复杂度O(1)
class Solution {
public int singleNumber(int[] nums) {
int[] count = new int[32]; //每个数32位二进制
for(int num : nums){
//对num的每一位统计右移(由低到高)
for(int i = 0; i < 32; i++){
count[i] += num & 1; //若该位为1
num >>= 1;
}
}
int res = 0;
//对结果的每一位赋值左移(由高到低)
for(int i = 0; i < 32; i++){
res <<= 1;
res |= count[31 - i] % 3; //或运算,0|1 = 1;0|0 = 0
}
return res;
}
}
- 有限状态自动机,每一位的余数有三种状态:0,1,2,使用二位二进制(t,o)表示这三种状态,使用异或运算和与运算得到状态转换公式(o = o ^ n & ~t,t = t ^ n & ~ o),因为每一位的步骤都一样,因此直接对数字进行状态转换计算,即可得到每位的状态, 最后的状态一定是00或01,返回o为1的位(即o)
- 时间复杂度O(n),空间复杂度O(1)
class Solution {
public int singleNumber(int[] nums) {
//有限状态自动机
int one = 0;
int two = 0;
for(int num : nums){
one = one ^ num & ~two;
two = two ^ num & ~one;
}
return one;
}
}
JZ57 和为s的两个数字
- 双指针,左右指针分别从数组开头和末尾出发,指向的数字和与目标值对比,小于目标值则左指针右移,大于目标值则右指针左移
- 时间复杂度O(n),空间复杂度O(1)
class Solution {
public int[] twoSum(int[] nums, int target) {
//双指针
int left = 0;
int right = nums.length - 1;
while(left < right){
if(target - nums[right] > nums[left]){
left++;
}else if(target - nums[right] < nums[left]){
right--;
}else{
return new int[]{nums[left], nums[right]};
}
}
return new int[2];
}
}
JZ57-II 和为s的连续正数序列
- 从1开始遍历求和,如果等于目标值,则记录到结果数组中,否则继续下一轮遍历,别用
- 时间复杂度O(n√n),空间复杂度O(1)
class Solution {
public int[][] findContinuousSequence(int target) {
List<int[]> res = new LinkedList<>();
int sum = 0;
int n = 0;
for(int i = 1; i <= target / 2; i++){
sum += i;
n = i;
while(sum < target){
n++;
sum += n;
}
if(sum == target){
int[] pathArr = new int[n - i + 1];
for(int k = 0; k <= n - i; k++){
pathArr[k] = k + i;
}
res.add(pathArr);
}
sum = 0;
}
int[][] result = new int[res.size()][];
for(int i = 0; i < res.size(); i++){
result[i] = res.get(i);
}
return result;
}
}
- 使用求和公式,已知目标和、左边界求右边界,由求等差数列求和公式(首项加末项) * 项数 / 2,可以得到末项的值即右边界,如果该右边界存在(为整数且大于左边界),则将序列保存到结果数组,否则计算下一个首项
- 时间复杂度O(n),空间复杂度O(1)
class Solution {
public int[][] findContinuousSequence(int target) {
List<int[]> res = new LinkedList<>();
//求和公式
for(int left = 1; left <= target / 2; left++){
//已知目标和、左边界求右边界
//注意left*left可能会溢出,因此需要使用long
double right = (Math.sqrt(1 + 4 * ((long)left * left - left + 2 * target)) - 1) / 2;
//如果该右边界存在(为整数且大于左边界),则将序列保存到结果数组
if(right == (int)right && (int)right > left){
int[] path = new int[(int)right - left + 1];
for(int i = 0; i <= (int)right - left; i++){
path[i] = i + left;
}
res.add(path);
}
}
return res.toArray(new int[0][]); //list转为array
}
}
- 双指针,左右指针都从数组开头出发(间隔1),指向的数字和与目标值对比,小于目标值则右指针右移,大于目标值则左指针左移
- 时间复杂度O(n),空间复杂度O(1)
class Solution {
public int[][] findContinuousSequence(int target) {
List<int[]> res = new LinkedList<>();
//双指针
int left = 1;
int right = 2;
int sum = left + right;
while(left < right && right <= target / 2 + 1){
if(sum < target){ //小于目标值则右指针右移
right++;
sum += right;
}else if(sum > target){ //大于目标值则左指针左移
sum -= left;
left++;
}else{ //和等于目标值
int[] path = new int[right - left + 1];
for(int i = 0; i <= right - left; i++){
path[i] = i + left;
}
res.add(path);
sum -= left;
left++;
right++;
sum += right;
}
}
// int[][] result = new int[res.size()][];
// for(int i = 0; i < res.size(); i++){
// result[i] = res.get(i);
// }
// return result;
return res.toArray(new int[0][]); //list转为array
}
}
JZ58-I 翻转单词顺序
- 使用栈,先去除字符串前后的空格(trim),然后从后向前遍历,首先压入一个空格,再将字符压入栈,不管碰到多少个空格都弹出为新字符,直到遍历完字符串
- 时间复杂度O(n),空间复杂度O(n)
class Solution {
public String reverseWords(String s) {
//使用栈,先去除字符串前后的空格
s = s.trim();
if(s.length() == 0) return s;
Deque<Character> stack = new LinkedList<>();
StringBuilder sb = new StringBuilder("");
//从后向前遍历,将字符压入栈
char[] sArr = s.toCharArray();
for(int i = sArr.length - 1; i >= 0; i--){
if(sArr[i] != ' '){
//首先压入一个空格,再将字符压入栈
if(stack.isEmpty()) stack.push(' ');
stack.push(sArr[i]);
}else{ //不管碰到多少个空格都弹出为新字符
while(!stack.isEmpty()){
sb.append(stack.pop());
}
}
}
//将最后一个单词弹出
while(!stack.isEmpty() && stack.peek() != ' '){
sb.append(stack.pop());
}
//去掉最后一个空格
//sb.deleteCharAt(sb.length() - 1);
return sb.toString();
}
}
- 双指针法,可以调用api(trim(),substring()),也可以自定义函数去除多余的空格(字符串前后的空格,字符串中间部分的多余空格),直接在原字符串上操作。然后将整个字符串反转,最后再将每个单词反转
- 时间复杂度O(n),空间复杂度O(n)
//调用api
class Solution {
public String reverseWords(String s) {
//双指针
//移除前后空格
s = s.trim();
if(s.length() == 0) return s;
//反向遍历
StringBuilder sb = new StringBuilder("");
int left = s.length() - 1;
int right = s.length() - 1;
while(left >= 0){
//找到单词前的第一个空格
while(left >= 0 && s.charAt(left) != ' ') left--;
sb.append(s.substring(left + 1, right + 1)); //切割单词
sb.append(" ");
//定位下一个单词的最后一个字母
while(left >= 0 && s.charAt(left) == ' ') left--;
right = left;
}
//去除最后一个空格
return sb.toString().trim();
}
}
//自定义函数
class Solution {
public String reverseWords(String s) {
//双指针
if(s.length() == 0) return s;
//自定义函数移除多余空格
String s2 = removeSpaces(s);
//将字符串转为字符数组
char[] sArr = s2.toCharArray();
//将整个字符串反转
reverseString(sArr, 0, sArr.length - 1);
//将每个单词反转
reverseWord(sArr);
return new String(sArr);
}
//反转字符串,输入:字符数组,反转起始,反转终止
private void reverseString(char[] sArr, int left, int right){
while(left < right){
//中间变量
char temp = sArr[left];
sArr[left] = sArr[right];
sArr[right] = temp;
//继续下一轮循环
left++;
right--;
}
}
//移除多余空格
private String removeSpaces(String s){
//定义左右指针
int left = 0;
int right = s.length() - 1;
//移除前后空格
while(left <= right && s.charAt(left) == ' ') left++;
while(left <= right && s.charAt(right) == ' ') right--;
//定义字符串数组返回结果
char[] res = new char[s.length()];
int i = 0;
//移除中间空格
for(;left <= right; left++){
//连续出现空格则跳过
if(left > 0 && s.charAt(left) == ' ' && s.charAt(left-1) == ' '){
continue;
}
//将字符填入数组
res[i++] = s.charAt(left);
}
return new String(res, 0, i); //左闭右开
}
//将每个单词反转
private void reverseWord(char[] sArr){
//定义快慢指针
int slow = 0;
for(int fast = 0; fast < sArr.length; fast++){
//快指针指向空格则进行反转
if(sArr[fast] == ' '){
reverseString(sArr, slow, fast - 1);
slow = fast + 1;
}
//快指针指向最后一位时反转最后一个单词
if(fast == sArr.length - 1){
reverseString(sArr, slow, fast);
slow = fast + 1;
}
}
}
}
JZ58-II 左旋转字符串
- 直接切片,偷懒做法
- 时间复杂度O(n),空间复杂度O(n)
class Solution {
public String reverseLeftWords(String s, int n) {
//切片
return s.substring(n, s.length()) + s.substring(0, n);
}
}
- 定义StringBuilder,遍历字符串然后拼接(求余简化代码),也可以直接字符串相加(效率低)
- 时间复杂度O(n),空间复杂度O(n)
class Solution {
public String reverseLeftWords(String s, int n) {
//定义StringBuilder,遍历字符串然后拼接(求余简化代码)
StringBuilder sb = new StringBuilder();
for(int i = n; i < s.length() + n; i++){
sb.append(s.charAt(i % s.length()));
}
return sb.toString();
}
}
- 使用StringBuilder或字符数组进行三次翻转,先翻转n前字符串,再翻转n后字符串,最后翻转整个字符串
- 时间复杂度O(n),空间复杂度O(1)
class Solution {
public String reverseLeftWords(String s, int n) {
//使用StringBuilder或字符数组进行三次翻转
StringBuilder sb = new StringBuilder(s);
//先翻转n前字符串
reverseString(sb, 0, n - 1);
//再翻转n后字符串
reverseString(sb, n, s.length() - 1);
//最后翻转整个字符串
reverseString(sb, 0, s.length() - 1);
return sb.toString();
}
//翻转字符串,输入待翻转字符串和左右区间(左闭右闭)
private void reverseString(StringBuilder sb, int left, int right){
while(left < right){
char temp = sb.charAt(left);
sb.setCharAt(left, sb.charAt(right));
sb.setCharAt(right, temp);
left++;
right--;
}
}
}