假设按照升序排序的数组在预先未知的某个点上进行了旋转。
( 例如,数组 [0,0,1,2,2,5,6] 可能变为 [2,5,6,0,0,1,2] )。
编写一个函数来判断给定的目标值是否存在于数组中。若存在返回 true,否则返回 false。
这是 搜索旋转排序数组 的延伸题目,本题中的 nums 可能包含重复元素。
这会影响到程序的时间复杂度吗?会有怎样的影响,为什么?
解答
public boolean search(int[] nums, int target) {
int left = 0;
int right = nums.length - 1;
//二分查找
while (left <= right) {
//去除左区间重复的数字
while (left < right && nums[left] == nums[left + 1]) left++;
//去除右区间重复的数字
while (left < right && nums[right] == nums[right - 1]) right--;
//中间位置
int mid = (left + right) / 2;
//若中间位置等于target则返回true
if (nums[mid] == target) return true;
//若中间位置小于右区间的值,则说明右边有序
if (nums[mid] <= nums[right]) {
//若target在这之间,则就是二分查找常规操作,修改左区间
if (target > nums[mid] && target <= nums[right]) left = mid + 1;
//否则寻找区间就是在中间位置的左边,修改右区间
else right = mid - 1;
}
//若左边有序,同理
else {
if (target >= nums[left] && target < nums[mid]) right = mid - 1;
else left = mid + 1;
}
}
return false;
}
分析
1.相比较之前的搜索旋转数组,这里数组中可能存在重复的值,所以需要过滤掉重复的数值。
即判断左区间所指位置和它的后一个位置数字是否相同,若相同则左区间缩小一,即加一操作.
右区间同理,若有相同的胡子,则右区间缩小一,即减一操作。
2.因为数组本来是有序的,经过旋转后,从中间一分为二,那么肯定可以确定的一点是一半是有序的数组,另一半则是无序的旋转数组。
所以在二分查找的基础上,要进行判断二分后,前半段和后半段哪一段是有序的。
再根据target是否在有序的范围内,进行修改左右区间。
给定一个排序链表,删除所有含有重复数字的节点,只保留原始链表中 没有重复出现 的数字。
public ListNode deleteDuplicates(ListNode head) {
if(head == null || head.next==null)return head;//若head为空或没有后继,则返回head
ListNode p = head.next;//p指向head的后一个
if(head.val == p.val){//若head指向的值和p指向的值相同,则需要修改头部指针的位置
while(p !=null && head.val == p.val){//直到p所指向的与头部的值不一致
p = p.next;
}
head = deleteDuplicates(p);//头部修改 deleteDuplicates(p)返回的值
}
//头部确定,修改之后的值
else {
head.next = deleteDuplicates(p);
}
return head;
}
分析
1.去除重复的数字的节点,分成两种情况
首先是头部所指的元素就是重复的,那么就需要头部指针
其次是去除除了头部之后重复的值。
2.举个例子来说明,例如1,1,1,2,2,3,4,5,6,6,7
首先 head指向第一个1,p指向head的后继,即第二个1
此时发现 head的值和p的值是一样的。所以需要一个while循环,找到与head不一样的值。
所以p指向了2.
调用递归,此时的head就相当于p。需要判断p作为头部的话,之后的有没有重复的值。
直到p指向3的时候,此时3作为头部,发现头部没有重复的值。即可确定头指针的位置。
之后就是判断头指针之后的数字是否重复。3->deleteDuplicates(p)
即head.next= deleteDuplicates(p);
可以发现,起始确定了头部之后,后面的递归操作起始就是在找子链表没有重复的头指针。
即整个过程就是 头指针数字重复,修改头指针,头指针不重复,确定头指针寻找之后子链表没有重复的头指针作为后继。
给定一个排序链表,删除所有重复的元素,使得每个元素只出现一次。
public ListNode deleteDuplicates2(ListNode head) {
if (head == null || head.next == null) return head;
ListNode p = head.next;
if (p.val == head.val) {
//过滤掉重复的数字
while (p != null && p.val == head.val) p = p.next;
}
head.next = deleteDuplicates2(p);
return head;
}
给定 n 个非负整数,用来表示柱状图中各个柱子的高度。每个柱子彼此相邻,且宽度为 1 。
求在该柱状图中,能够勾勒出来的矩形的最大面积。
以上是柱状图的示例,其中每个柱子的宽度为 1,给定的高度为 [2,1,5,6,2,3]。
图中阴影部分为所能勾勒出的最大矩形面积,其面积为 10 个单位。
//方法1
public static int largestRectangleArea(int[] heights) {
int[] heightsCopy = Arrays.copyOf(heights, heights.length);
Arrays.sort(heightsCopy);//拷贝一份用于排序
int largestArea = 0;
//从最低的树高开始遍历
for (int i = 0; i < heightsCopy.length; i++) {
int width = 0;
int maxWidth = 0;
//找到满足最低树高的最大宽度
for (int j = 0; j < heights.length; j++) {
if (heights[j] >= heightsCopy[i]) {
width++;
if (width > maxWidth) maxWidth = width;
} else {
width = 0;
}
}
//对大宽度*树高,则是这个高度的最大面积,更新最大面积
if (largestArea < maxWidth * heightsCopy[i]) {
largestArea = maxWidth * heightsCopy[i];
}
}
return largestArea;
}
//方法二
public int largestRectangleArea2(int[] heights) {
int heightCopy[] = new int[heights.length + 1];
for (int i = 0; i < heights.length; i++) {
heightCopy[i] = heights[i];
}
heightCopy[heights.length] = 0; //最后增加个高度为0 的柱子,以便吧单调栈里面的都弹出去。
//单调栈
Deque<Integer> stack = new ArrayDeque<>(); //存储序号
int maxS = 0;
for (int i = 0; i < heightCopy.length; i++) {
while (!stack.isEmpty() && heightCopy[i] < heightCopy[stack.peek()]) { //一直出栈 直到遇见小的
int temp = stack.pop();
maxS = Math.max(maxS, ((stack.isEmpty() ? i : (i - stack.peek() - 1)) * heights[temp]));
}
stack.push(i); //入栈
}
return maxS;
}
分析
1.方法一比较简单,就是将所有的柱子高低排序,从最低的一根开始。
2.计算满足最低高度的最大宽度。两者的乘积就是面积。然后迭代最低高度,得到新的面积,以此来更新最大面积。
3.方法二利用了一个单调栈,栈内存入的只能是单调递增的柱子序号。当遇到一个柱子比栈内的柱子矮的时候,则出栈。
出栈后计算满足出栈柱子高度的个数。个数就是(stack.isEmpty() ? i : (i - stack.peek() - 1))。个数乘以柱子高度就是面积。更新最大面积。
给定一个仅包含 0 和 1 的二维二进制矩阵,找出只包含 1 的最大矩形,并返回其面积。
public static int maximalRectangle(char[][] matrix) {
if (matrix.length == 0 || matrix[0].length == 0) return 0;
int max = 0;
int[] heights = new int[matrix[0].length];
// 每一层计算高度 然后调用上一题的解法
for (int i = 0; i < matrix.length; i++) {
for (int j = 0; j < matrix[0].length; j++) {
heights[j] = matrix[i][j] == '1' ? heights[j] + 1 : 0;
}
max = Math.max(max, largestRectangleArea2(heights));
}
return max;
}
public static int largestRectangleArea2(int[] heights) {
int heightCopy[] = new int[heights.length + 1];
for (int i = 0; i < heights.length; i++) {
heightCopy[i] = heights[i];
}
heightCopy[heights.length] = 0; //最后增加个高度为0 的柱子,以便吧单调栈里面的都弹出去。
Deque<Integer> stack = new ArrayDeque<>(); //存储序号
int maxS = 0;
for (int i = 0; i < heightCopy.length; i++) {
while (!stack.isEmpty() && heightCopy[i] < heightCopy[stack.peek()]) { //一直出栈 直到遇见小的
int temp = stack.pop();
maxS = Math.max(maxS, ((stack.isEmpty() ? i : (i - stack.peek() - 1)) * heights[temp]));
}
stack.push(i); //入栈
}
return maxS;
}
给定一个链表和一个特定值 x,对链表进行分隔,使得所有小于 x 的节点都在大于或等于 x 的节点之前。
你应当保留两个分区中每个节点的初始相对位置。
public static ListNode partition(ListNode head, int x) {
ListNode auxiliaryHead1 = new ListNode(0);//用于存储小于x的值的结点
ListNode auxiliaryHead2 = new ListNode(0);//用于存储大于x值的结点。
ListNode node1 = auxiliaryHead1;
ListNode node2 = auxiliaryHead2;
while (head != null) {
if (head.val < x) {//小于x值的结点,用尾插法,插入到第一个链表
node1.next = head;
head = head.next;
node1 = node1.next;
node1.next = null;
} else {//大于等于x值的结点,同样用尾插法,插入到第二个链表
node2.next = head;
head = head.next;
node2 = node2.next;
node2.next = null;
}
}
//第一个链表尾部连接第二个链表(除辅助结点,即头结点不算在内。)
node1.next = auxiliaryHead2.next;
return auxiliaryHead1.next;
}
给定一个字符串 s1,我们可以把它递归地分割成两个非空子字符串,从而将其表示为二叉树。
下图是字符串 s1 = “great” 的一种可能的表示形式。
在扰乱这个字符串的过程中,我们可以挑选任何一个非叶节点,然后交换它的两个子节点。
例如,如果我们挑选非叶节点 “gr” ,交换它的两个子节点,将会产生扰乱字符串 “rgeat” 。
我们将 "rgeat” 称作 “great” 的一个扰乱字符串。
同样地,如果我们继续交换节点 “eat” 和 “at” 的子节点,将会产生另一个新的扰乱字符串 “rgtae” 。
我们将 "rgtae” 称作 “great” 的一个扰乱字符串。
给出两个长度相等的字符串 s1 和 s2,判断 s2 是否是 s1 的扰乱字符串。
public boolean isScramble(String s1, String s2) {
char[] chs1 = s1.toCharArray();
char[] chs2 = s2.toCharArray();
int n = s1.length();
int m = s2.length();
if (n != m) {
return false;
}
boolean[][][] dp = new boolean[n][n][n + 1];//表示字符串s1 从i开始 长度为len的字符串 是否可以转换为s2 从j开始长度为len 的字符串
// 初始化单个字符的情况
for (int i = 0; i < n; i++) {
for (int j = 0; j < n; j++) {
dp[i][j][1] = chs1[i] == chs2[j];
}
}
// 枚举区间长度 2~n
for (int len = 2; len <= n; len++) {
// 枚举 S1 中的起点位置
for (int i = 0; i <= n - len; i++) {
// 枚举 s2 中的起点位置
for (int j = 0; j <= n - len; j++) {
// 枚举划分位置
for (int k = 1; k <= len - 1; k++) {
// 第一种情况:分割没交换
if (dp[i][j][k] && dp[i + k][j + k][len - k]) {
dp[i][j][len] = true;
break;
}
// 第二种情况:分割且交换
if (dp[i][j + len - k][k] && dp[i + k][j][len - k]) {
dp[i][j][len] = true;
break;
}
}
}
}
}
return dp[0][0][n];
}
分析
1.若s1和s2长度不一致 则肯定不能变成
2.两个字符串 根据同一个位置分割,字符串s1可以分成两段s11和s12,字符串s2分成s21和s22。这是可以分两种情况讨论
* 分割后不交换,s11 对应s21, s12 对应s22
* 分割后交换,s11 对应s22, s12 对应s21
此时转移方程可列出
情况一
dp[i][j][len] = dp[i][j][k] && dp[i + k][j + k][len - k]。
根据k点分割。
字符串s1从起点i长度为len和字符串s2从起点j长度为len 是否满足扰乱字符串。取决于从k点分割后,两个子串是否满足扰乱字符串。
这是情况1,分割后不交换。那么前一段就表示dp[i][j][k],后一段则表示dp[i + k][j + k][len - k]
情况二
dp[i][j][len] = dp[i][j + len - k][k] && dp[i + k][j][len - k]
根据k点分割后s2交换子字符串。
此时s21的起始点为j+len-k。len-k就是相对于原来的起始点位移的距离。
s22的起始点为j。变为原来s21的起始点。
给你两个有序整数数组 nums1 和 nums2,请你将 nums2 合并到 nums1 中,使 num1 成为一个有序数组。
说明:
public static void merge(int[] nums1, int m, int[] nums2, int n) {
int flag = 0;//用于记录已经完成插入的位置
//遍历nums2
for (int i = 0; i < n; i++) {
//从flag位置遍历nums1
for (int j = flag; j < nums1.length; j++) {
//若找到比nums2[i]大的位置,则这位置之后的数字后移
if (nums2[i] < nums1[j]) {
flag = j;//修改flag,减少遍历的次数
//后移
for (int k = m - 1; k >= flag; k--) {
nums1[k + 1] = nums1[k];
}
//插入数字,flag后移一位
nums1[flag++] = nums2[i];
m++;//nums1中有效数字+1
break;
} else if (j >= m) {
//若超过有效数字,则直接插入在末尾
nums1[j] = nums2[i];
flag++;
m++;
break;
}
}
}
}
格雷编码是一个二进制数字系统,在该系统中,两个连续的数值仅有一个位数的差异。
给定一个代表编码总位数的非负整数 n,打印其格雷编码序列。格雷编码序列必须以 0 开头。
public static List<Integer> grayCode(int n) {
List<Integer> ret = new ArrayList<>();
for(int i = 0; i < 1<<n; ++i)
ret.add(i ^ i>>1);
return ret;
}
分析
1.根据格雷编码的生成过程
G(i) = i ^ (i/2);
如 n = 3:
G(0) = 000,
G(1) = 1 ^ 0 = 001 ^ 000 = 001
G(2) = 2 ^ 1 = 010 ^ 001 = 011
G(3) = 3 ^ 1 = 011 ^ 001 = 010
G(4) = 4 ^ 2 = 100 ^ 010 = 110
G(5) = 5 ^ 2 = 101 ^ 010 = 111
G(6) = 6 ^ 3 = 110 ^ 011 = 101
G(7) = 7 ^ 3 = 111 ^ 011 = 100
一个二进制数 和 自身右移一位之后的二进制数做或运算。
给定一个可能包含重复元素的整数数组 nums,返回该数组所有可能的子集(幂集)。
说明:解集不能包含重复的子集。
public List<List<Integer>> subsetsWithDup(int[] nums) {
List<List<Integer>> res = new ArrayList<>();
Arrays.sort(nums);
int len = nums.length;
backtrack(res, new ArrayList<>(), nums, len, 0);
return res;
}
public void backtrack(List<List<Integer>> res, List<Integer> tmp, int[] nums, int len, int index) {
res.add(new ArrayList<>(tmp));
for (int i = index; i < len; ++i) {
if (i - 1 >= index && nums[i - 1] == nums[i])//去重
continue;
tmp.add(nums[i]);
backtrack(res, tmp, nums, len, i + 1);
tmp.remove(tmp.size() - 1);
}
}
分析
1.又是一道组合的题目,用回溯实现。
2.这题主要难在去掉重复的子集。例如[1,2,2]
(i - 1 >= index && nums[i - 1] == nums[i]) 去掉同一层递归中,相同的数字。因为添加了
i - 1 >= index 这一个条件。所以不会妨碍到 不用层递归相同的数字添加的情况。
比如第一层调用 加入数字是2,递归调用第二层加入的数字是2,若此时没有(i - 1 >= index),则就不会添加进去。就会少了[2,2]和[1,2,2]的答案。
一条包含字母 A-Z 的消息通过以下方式进行了编码:
给定一个只包含数字的非空字符串,请计算解码方法的总数。
public int numDecodings(String s) {
final int length = s.length();
if(length == 0) return 0;//字符串为空 返回0
if(s.charAt(0) == '0') return 0;//首个数字为0,则返回0
int[] dp = new int[length+1];
dp[0] = 1;
for (int i = 0; i < length; i++) {
//首先讨论 该位单独组成数字的情况
dp[i + 1] = s.charAt(i) == '0' ? 0 : dp[i];//初始化,该位数字不为0,则和该位前一位的组合数量一致。若为0,则不能组成数字。
//然后讨论,该位与前一位组成数字的情况
// 这一位数字和前一位数字能组成1-26的数字。那么这一位上的组合数量,就加上这两个数字之前的组合数量。
if (i > 0 && (s.charAt(i - 1) == '1' || (s.charAt(i - 1) == '2' && s.charAt(i) <= '6'))) {
dp[i + 1] += dp[i - 1];
}
}
return dp[length];
}
分析
1.利用动态规划实现,dp[i]表示第i位数字(包括本身)之前能组合的数量。
2.每一位有两种情况组成字母
第一种,自身对应字母,若自身不为0,则说明有字母对应,所以此时的组合数量和前一位的组合数量一致
即 dp[i + 1] = s.charAt(i) == ‘0’ ? 0 : dp[i];
第二种,自身和前一位数字组合,对应某一个字母。这里就需要满足两个条件,前一位数字必须是1或2,该位数字必须是0-6.这样该位置的组合数在 第一种的情况下 再加上 除了这两位之前的数字的组合情况。
即 dp[i + 1] += dp[i - 1];
反转从位置 m 到 n 的链表。请使用一趟扫描完成反转。
说明:
1 ≤ m ≤ n ≤ 链表长度。
public ListNode reverseBetween(ListNode head, int m, int n) {
ListNode dummy = new ListNode(0);//虚拟头结点
dummy.next = head;
ListNode pre = dummy;
for(int i = 1; i < m; i++){
pre = pre.next;//用来找到插入位置的前驱
}
head = pre.next;//需要翻转的链表的头
for(int i = m; i < n; i++){//需要翻转的链表,每个结点都插入到pre之后的位置。
ListNode nex = head.next;
head.next = nex.next;
nex.next = pre.next;
pre.next = nex;
}
return dummy.next;
}
分析
1.设置一个虚拟头结点,可以避免需要翻转的链表是从头部开始的 而需要另外另外的指针帮助旋转。简化步骤
2.首先将head接在虚拟头结点后面,然后找到需要翻转的链表的位置。设置插入的位置,因为要翻转,所以每次的插入的位置都是一样的。
3.之后遍历需要翻转的链表。结点一个一个的插入到需要插入的位置即可
给定一个只包含数字的字符串,复原它并返回所有可能的 IP 地址格式。
public List<String> restoreIpAddresses(String s) {
//字符串不符合长度 则直接返回。
if (s == null || s.length() < 4 || s.length() > 12) return new ArrayList<>();
List<String> result = new ArrayList<>();
restoreIpAddresses(s, 0, new ArrayList<>(), result);
return result;
}
public void restoreIpAddresses(String s, int index, List<String> list, List<String> result) {
// 剩余最长字符串长度。list中是已经确定的字符串。
int maxLength = (4 - list.size()) * 3;
// 如果原字符串剩余字符 大于预期最大长度 不符合要求
if (s.length() - index > maxLength) {
return;
}
// 若list大小为4,或这字符串s遍历到了最后,则说明满足条件。找到了字符串组合。
if (list.size() == 4 && s.length() == index) {
// 拼接
StringBuilder sb = new StringBuilder();
for (int i = 0; i < list.size(); i++) {
if (i == 0) {
sb.append(list.get(i));
} else {
sb.append(".").append(list.get(i));
}
}
result.add(sb.toString());
}
//在确定起点index的基础上 向后遍历范围3以内的字符。
for (int i = index + 1; i <= index + 3 && i <= s.length(); i++) {
String ip = s.substring(index, i);
// 大于255 不符合ip规则
if (Integer.parseInt(ip) > 255)
continue;
// 大于1位数时 0不能为头
if (ip.length() > 1 && "0".equals(ip.substring(0, 1)))
continue;
list.add(ip);
restoreIpAddresses(s, i, list, result);
list.remove(list.size() - 1);
}
}
分析
1.首先ip地址转换为十进制后,分成4个数字组成,每个数字最大255.所以字符串长度 最长只能是12.
并且必须分成4组。所以必须有4个数字组成,最短长度为4. 不符合4-12长度的字符串不需要考虑。
2.分成的4个数字中,大于0的数字,不能以0打头。
3.遍历字符串,每个字符以区间3范围内寻找满足条件的字符串。
4.当找到一个字符串,计算剩余的字符串长度 s.length() - index,查看是否满足大于 余下字符串需满足的最大长度(4 - list.size()) * 3;若不满足则回溯,去掉刚找到的字符串。
5.当满足找到的字符串组合数等于4 并且遍历完了字符串s,则将找到的字符串组合拼接起来。加入到答案集合中。
给定一个二叉树,返回它的中序 遍历。
//方法一 递归算法
public List<Integer> inorderTraversal(TreeNode root) {
List<Integer> res = new ArrayList<>();
inorderTraversal(root,res);
return res;
}
public void inorderTraversal(TreeNode root,List<Integer> res){
if(root==null)return;
//递归左子树中序遍历
inorderTraversal(root.left,res);
//访问
res.add(root.val);
//递归右子树中序遍历
inorderTraversal(root.right,res);
}
//方法二 迭代算法
public List<Integer> inorderTraversal(TreeNode root) {
List<Integer> res = new ArrayList<>();
Stack<TreeNode> stack = new Stack<>();
TreeNode p = root;
//当p不为空或者占栈不为空的时候
while (p != null || !stack.isEmpty()) {
//若p不为空,则将p入栈,p指向它的左孩子
if (p != null) {
stack.push(p);
p = p.left;
}
//若p为空,则取出栈顶元素,读取其值,p指向它的右孩子。
else {
p = stack.pop();
res.add(p.val);
p = p.right;
}
}
return res;
}
分析
1.二叉树中序遍历的顺序就是先访问左孩子再访问根结点最后访问右孩子。
2.方法一 首先访问左孩子若不是叶子结点则递归调用将左孩子作为根结点,进行中序遍历
若是叶子结点则跳到上一层递归,读取该结点值,然后访问右孩子,若右孩子不是叶子结点则递归调用将右孩子作为根结点,进行中序遍历
3.方法二 利用栈来实现递归的过程,p指向根结点开始,有左孩子就将左孩子入栈,p指向左孩子。当p为空,则说明已经找到了最左边的叶子结点,出栈访问它,将p指向它的右孩子。一直迭代直到p为空 且栈为空。
提交结果
给定一个整数 n,生成所有由 1 … n 为节点所组成的二叉搜索树。
示例:
public List<TreeNode> generateTrees(int n) {
if(n == 0)return new ArrayList<>();
return generateTrees(1, n);
}
public List<TreeNode> generateTrees(int i, int j) {
List<TreeNode> res = new ArrayList<>();
if (i > j) {
res.add(null);
return res;
}
if (i == j) {//单个结点直接返回
TreeNode treeNode = new TreeNode(i);
res.add(treeNode);
return res;
}
for (int k = i; k <= j; k++) {
// 以k为根,找左子树的组合
List<TreeNode> LeftLists = generateTrees(i, k - 1);
// 以k为根,找右子树的组合
List<TreeNode> RightLists = generateTrees(k + 1, j);
// 遍历左右子树组合 即可得到以k为根所有可能的情况
for (TreeNode leftNode : LeftLists) {
for (TreeNode rightNode : RightLists) {
TreeNode newRoot = new TreeNode(k);
newRoot.left = leftNode;
newRoot.right = rightNode;
res.add(newRoot);
}
}
}
return res;
}
分析
1.最开始想到的是使用回溯来做,但后面发现会出现重复解的情况,没想到很好的办法解决。
2.利用搜索二叉树的特点,左子树所有的值比根结点的值小,右子树所有的值比根结点的值要大。那么就可以使用递归来实现。
3.每次选择一个值作为根。那么比他小的所有值必定是在左子树上,比他大的所有值必定是在右子树上。同理递归去判断左右子树的情况。确定根,遍历左右子树可能的情况的组合 即可得到最后的答案。
给定一个整数 n,求以 1 … n 为节点组成的二叉搜索树有多少种?
public int numTrees(int n) {
if (n < 3)return n;
int[] dp = new int[n + 1];
dp[0] = 1;
dp[1] = 1;
dp[2] = 2;
// 动态规划
for (int i = 3; i <= n; i++) {
for (int j = 0; j < i; j++) {
dp[i] += dp[j] * dp[i - j - 1];
}
}
return dp[n];
}
分析
1.首先n等于1 的时候 只有1种情况
n等于2的时候,有2种情况:1作为根结点或者2作为根结点
n等于3的时候,可以考虑1-3分别做一次根结点,
当1作为根结点的时候,那么2和3只能在右子树出现。所以左子树为空,右子树的情况就是2和3可能的组合,也就是n=2的时候的组合数量即等于2.所以当1作为根结点的时候,组合的数量=12=2。
当2作为根结点的时候,那么1和3只能分别在其左右孩子结点上。所以此时的组合数=11=1。
同理3作为根结点的时候,组合数量=2*1=2.
所以dp[3] = dp[0]*dp[2] + dp[1]*dp[1] + dp[2]*dp[0]
n等于4的时候,同样是考虑1-4分别做一次根结点。
当1作为根结点的时候,234只能在右子树出现,所以左子树为空,右子树看你的组合数量 就是 n=3的时候的组合数量,即等于5。
当2作为根结点的时候,1在左子树,34在右子树,左子树的组合数为1,右子树的组合数就等于n=2的时候的组合数量,即等于2。
3,4做根结点同理。
所以dp[4] = dp[0]*dp[3] + dp[1]*dp[2] +dp[2]*dp[1] + dp[3]*dp[0]
根据上述分析可以得出动态方程
for (int i = 3; i <= n; i++) {
for (int j = 0; j < i; j++) {
dp[i] += dp[j] * dp[i - j - 1];
}
}
给定三个字符串 s1, s2, s3, 验证 s3 是否是由 s1 和 s2 交错组成的。
//方法一
public static boolean isInterleave(String s1, String s2, String s3) {
if (s3.length() != s1.length() + s2.length()) return false;
boolean[][] dp = new boolean[s1.length() + 1][s2.length() + 1];
for (int i = 0; i <= s1.length(); i++) {
for (int j = 0; j <= s2.length(); j++) { // 两个空字符串 交错组成 空字符串 为true
if (i == 0 && j == 0) dp[i][j] = true;
// s2为空,那么dp[0][j]就是判断 s1的前j个 和s3的前j个是否相同
else if (i == 0)
dp[i][j] = dp[i][j - 1] && s2.charAt(j - 1) == s3.charAt(j - 1);
// s1位空,那么dp[i][0]就是判断 s2的前i个 和s3 的前i个是否相同
else if (j == 0)
dp[i][j] = dp[i - 1][j] && s1.charAt(i - 1) == s3.charAt(i - 1);
// 其余时候,判断s1的前i个和s2的前j个 是否可以组成s3的前i+j个。分成两种情况
// 情况一:s1的前i-1个和s2的前j个 可以组成s3的前i+j-1个 并且 s1的第i个元素 等于 s3的第i+j个元素。则为true。即 dp[i][j] = dp[i - 1][j] && s1.charAt(i - 1) == s3.charAt(i + j - 1)
// 情况二:s1的前i个和s2的前j-1个 可以组成s3的前i+j-1个 并且 s2的第j个元素 等于 s3的第i+j个元素。则为true。即 dp[i][j - 1] && s2.charAt(j - 1) == s3.charAt(i + j - 1)
else
dp[i][j] = (dp[i][j - 1] && s2.charAt(j - 1) == s3.charAt(i + j - 1)) || (dp[i - 1][j] && s1.charAt(i - 1) == s3.charAt(i + j - 1));
}
}
return dp[s1.length()][s2.length()];
}
//方法二
public boolean isInterleave2(String s1, String s2, String s3) {
if (s3.length() != s1.length() + s2.length())
return false;
boolean dp[] = new boolean[s2.length() + 1];
for (int i = 0; i <= s1.length(); i++) {
for (int j = 0; j <= s2.length(); j++) {
if (i == 0 && j == 0)
dp[j] = true;
else if (i == 0)
dp[j] = dp[j - 1] && s2.charAt(j - 1) == s3.charAt(i + j - 1);
else if (j == 0)
dp[j] = dp[j] && s1.charAt(i - 1) == s3.charAt(i + j - 1);
else
dp[j] = (dp[j] && s1.charAt(i - 1) == s3.charAt(i + j - 1)) || (dp[j - 1] && s2.charAt(j - 1) == s3.charAt(i + j - 1));
}
}
return dp[s2.length()];
}
分析
1.使用dp实现。dp[i][j]表示 s1的前i个元素和s2的前j个元素 是否可以组成s3的前i+j个元素
2.当s1为空的时候,dp[0][j] 表示 s2的前j个元素 和s3的前j个元素是否相同,相同则说明可以组成
当s2为空的时候,dp[i][0] 表示 s1的前i个元素 和s3的前i个元素是否相同,相同则说明可以组成
3.当均不为空的时候,判断s1的前i个和s2的前j个 是否可以组成s3的前i+j个,分成两种情况。
情况一:s1的前i-1个和s2的前j个 可以组成s3的前i+j-1个 并且 s1的第i个元素 等于 s3的第i+j个元素。则为true。即 dp[i][j] = dp[i - 1][j] && s1.charAt(i - 1) == s3.charAt(i + j - 1)
情况二:s1的前i个和s2的前j-1个 可以组成s3的前i+j-1个 并且 s2的第j个元素 等于 s3的第i+j个元素。则为true。即 dp[i][j - 1] && s2.charAt(j - 1) == s3.charAt(i + j - 1)
4.方法二 是在二维dp的基础上的改进。
根据动态规划方程可以发现,dp[i][j]和dp[i-1][j]、dp[i][j-1]有关,所以可以使用一维数组来替代即滚动数组。
就改成了dp[i]的值和当前dp[i]的值和之前一位dp[i-1]有关
当前的dp[i]就相当于原来二维数组里的dp[i-1][j]
dp[i-1]就相当于原来二维数组里的dp[i][j-1]
提交结果
给定一个二叉树,判断其是否是一个有效的二叉搜索树。
假设一个二叉搜索树具有如下特征:
节点的左子树只包含小于当前节点的数。
节点的右子树只包含大于当前节点的数。
所有左子树和右子树自身必须也是二叉搜索树。
示例 1:
示例 2:
//方法一
public static boolean isValidBST(TreeNode root) {
if (root == null || root.left == null && root.right == null) return true;
Stack<TreeNode> stack = new Stack<>();
TreeNode p = root;
long number = Long.MIN_VALUE;
//中序遍历
while (p != null || !stack.isEmpty()) {
if (p != null)
stack.push(p);
if (p != null && p.left != null)
p = p.left;
else {
TreeNode q = stack.pop();
if (q.val <= number) return false;//若当前的值比前一个值小,返回false
else {
number = q.val;//记录当前值
p = q.right;
}
}
}
return true;
}
//方法二
long last = Long.MIN_VALUE;
public boolean isValidBST(TreeNode root) {
if (root == null) return true;
if (isValidBST(root.left)) {
if (last < root.val) {
last = root.val;
return isValidBST(root.right);
}
}
return false;
}
分析
1.因为二叉搜索树的中序遍历是有序的。是一个递增序列。
所以可以利用中序遍历来遍历树。若遍历过程中 当前的值比前一个值小,说明不满足二叉搜索树。
2.方法二是使用递归实现中序遍历。
二叉搜索树中的两个节点被错误地交换。
请在不改变其结构的情况下,恢复这棵树。
使用 O(n) 空间复杂度的解法很容易实现。
你能想出一个只使用常数空间的解决方案吗?
解答
public void recoverTree(TreeNode root) {
List<Integer> nums = new ArrayList();
midOrder(root, nums);
int[] swapped = swap(nums);
recover(root, 2, swapped[0], swapped[1]);
}
//中序遍历 结果放在集合nums中
public void midOrder(TreeNode root, List<Integer> nums) {
if (root == null) return;
midOrder(root.left, nums);
nums.add(root.val);
midOrder(root.right, nums);
}
//找到需要交换的两个数字
public int[] swap(List<Integer> nums) {
int a = -1, b = -1;
for (int i = 0; i < nums.size() - 1; i++) {
if (nums.get(i + 1) < nums.get(i)) {
a = nums.get(i + 1);
if(b==-1) b = nums.get(i);
else break;
}
}
return new int[]{a, b};
}
//先序遍历 找到要交换的两个数字 并进行替换
public void recover(TreeNode r, int count, int a, int b) {
if (r != null) {
if (r.val == a || r.val == b) {
r.val = r.val == a ? b : a;
if (--count == 0) return;
}
recover(r.left, count, a, b);
recover(r.right, count, a, b);
}
}
分析
1.中序遍历的结果必定是有序递增的,若发现有前者大于后者的 说明这里出了问题 需要交换。
根据示例1 可知中序遍历的结果是321。若当发现3大于2的时候就确定这两位交换的话,则会出现错误。所以需要再往后找,找一个更小的。
if (nums.get(i + 1) < nums.get(i)) {
a = nums.get(i + 1);
if(b==-1) b = nums.get(i);
else break;
}
2.知道交换的数字后,再遍历一次树 替换相关的结点的值。
给定两个二叉树,编写一个函数来检验它们是否相同。
如果两个树在结构上相同,并且节点具有相同的值,则认为它们是相同的。
public boolean isSameTree(TreeNode p, TreeNode q) {
if (p == null && q == null) return true;
if ((p == null && q != null) || (p != null && q == null)) return false;//当前比较的两个节点有个不存在 则返回false
// 中序遍历
if (isSameTree(p.left, q.left)) {
if (p.val == q.val) {//若当前两个节点值相同则递归判断其右孩子
return isSameTree(p.right, q.right);
}
}
return false;
}