数据结构预算法——刷题记录二
仅用于记录自己的刷题过程
1. 二分查找/排序
1. BM22 比较版本号
题目:
解析:
思路一:
分别遍历来两个字符串,将其在点号之间的数字变成数字比较。
step 1:利用两个指针表示字符串的下标,分别遍历两个字符串。
step 2:每次截取点之前的数字字符组成数字,即在遇到一个点之前,直接取数字,加在前面数字乘10的后面。(因为int会溢出,这里采用long记录数字)
step 3:然后比较两个数字大小,根据大小关系返回1或者-1,如果全部比较完都无法比较出大小关系,则返回0.
import java.util.*;
public class Solution {
/**
* 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
*
* 比较版本号
* @param version1 string字符串
* @param version2 string字符串
* @return int整型
*/
public int compare (String version1, String version2) {
int n1 = version1.length();
int n2 = version2.length();
int i = 0, j = 0;
while (i < n1 || j < n2) {
long num1 = 0, num2 = 0;
while ( i < n1 && version1.charAt(i) != '.') {
num1 = num1 * 10 + (version1.charAt(i) - '0');
i++;
}
i++;
while ( j < n2 && version2.charAt(j) != '.') {
num2 = num2 * 10 + (version2.charAt(j) - '0');
j++;
}
j++;
if (num1 > num2)
return 1;
if (num1 < num2)
return -1;
}
return 0;
}
}
注意:
1、这里的long num1 = 0, num2 = 0;
要放在while循环内部,因为每次比较都是比较两个点之间的数字(以点为界限),如果不放在while循环内部那么就成了将整个字符串变成对应数字,那1.0和1.0.0就会判断错误。
2、***.charAt(i) - '0'
:ASC码48就是’0’,也就是说’0’的值是48,而后依次是’1’到’9’。 这样正好是char型减去48就是它对应的int值。
也就是 char转换成int是char- '0'
;int转换成char是char + '0'
。
思路二:
直接分割后比较。
step 1:使用split函数或者字符串流输入,按照点将两个原始字符串分割,使每个修订号的数字单独呈现在数组中。
step 2:遍历数组,每次各自取出一个数字比较,较短的版本号没有可取的数字了,就直接取0。
step 3:遍历取出的数字字符串,将其转换成数字,然后比较数字大小。根据大小关系返回1或者-1,如果全部比较完都无法比较出大小关系,则返回0.
import java.util.*;
public class Solution {
/**
* 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
*
* 比较版本号
* @param version1 string字符串
* @param version2 string字符串
* @return int整型
*/
public int compare (String version1, String version2) {
// 原本version1 = “1.01”,version2“1.001”
// 经过.split("\\.") 变成 nums1 = ["1","01"],nums2 = ["1","001"]
String[] nums1 = version1.split("\\.");
String[] nums2 = version2.split("\\.");
for (int i = 0; i < nums1.length || i < nums2.length; i++) {
String str1 = i < nums1.length ? nums1[i] : "0";
String str2 = i < nums2.length ? nums2[i] : "0";
long num1 = 0;
//字符串转数字
for (int j = 0; j < str1.length(); j++)
num1 = num1 * 10 + (str1.charAt(j) - '0');
long num2 = 0;
for (int j = 0; j < str2.length(); j++)
num2 = num2 * 10 + (str2.charAt(j) - '0');
//比较数字大小
if (num1 > num2)
return 1;
if (num1 < num2)
return -1;
}
//版本相同
return 0;
}
}
2. BM20 数组中的逆序对
题目:
解析:
思路一:
利用归并排。其实就是在归并排序的过程中加上计数。
step 1: 划分阶段:将待划分区间从中点划分成两部分,两部分进入递归继续划分,直到子数组长度为1.
step 2: 排序阶段:使用归并排序递归地处理子序列,同时统计逆序对,因为在归并排序中,我们会依次比较相邻两组子数组各个元素的大小,并累计遇到的逆序情况。而对排好序的两组,右边大于左边时,它大于了左边的所有子序列,基于这个性质我们可以不用每次加1来统计,减少运算次数。
step 3: 合并阶段:将排好序的子序列合并,同时累加逆序对。
public class Solution {
public int mod = 1000000007;
public int InversePairs(int[] array) {
int len = array.length;
if (len < 2 ) {
return 0;
}
int[] temp = new int[len];
int counts = msort(array, 0, len - 1, temp);
return counts;
}
public int msort(int[] nums, int left, int right, int[] temp) {
// 递归终止条件
if (left == right) {
return 0;
}
// 当left和right非常大的时候,直接使用int mid = (left + righ) / 2容易发生整型溢出
int mid = left + (right - left) / 2;
int leftcount = msort(nums, left, mid, temp);
int rightcount = msort(nums, mid + 1, right, temp);
// 优化,如果左半部分最后一个元素小于右半部分第一个元素
if (nums[mid] <= nums[mid + 1]) {
return leftcount + rightcount;
}
// 防止溢出,在此处先mod一次,之后再mod一次,不然等后面一次解决mod会发生溢出
int tempcount = (leftcount + rightcount) % mod;
int count = merge(nums, left, mid, right, temp);
// return (leftcount + rightcount +count) % mod;
return (tempcount + count ) % mod;
}
public int merge(int[] nums, int left, int mid, int right, int[] temp) {
for (int i = left ; i <= right ; i++) {
temp[i] = nums[i];
}
int i = left;
int j = mid + 1;
int count = 0;
for (int k = left ; k <= right ; k++) {
if (i == mid + 1) { // 左半部分已经排序完
nums[k] = temp[j];
j++;
} else if ( j == right + 1) { // 右半部分已经排序完
nums[k] = temp[i];
i++;
} else if (temp[i] <= temp[j]) {// 这里要是保证归并排序是稳定排序,就必须加上等于temp[i] <= temp[j]
nums[k] = temp[i];
i++;
} else {
nums[k] = temp[j];
j++;
count += (mid - i + 1);
}
}
return count;
}
}
注意:
1、返回的时候不能直接return leftcount + rightcount +count;
然后再对结果mod,也不能直接return (leftcount + rightcount +count) % mod;
,这样在数字很大的时候会发生溢出。所以应该分成两部分,先对(leftcount + rightcount)%mod
,然后将其结果和count相加后再次mod.
2、temp[i] <= temp[j]
这里要是保证归并排序是稳定排序,就必须加上等于,temp[i] <= temp[j]。
3、求中间值的时候需要按照 int mid = left + (right - left) / 2
这样计算,因为当left和right非常大的时候,直接使用 int mid = (left + righ)) / 2
容易发生整型溢出。
思路二:树状数组。不会。。。。
3. BM23 二叉树的前序遍历
题目:
给你二叉树的根节点 root ,返回它节点值的 前序 遍历。
数据范围:二叉树的节点数量满足 0≤n≤100 ,二叉树节点的值满足 0 \1≤val≤100 ,树的各节点的值各不相同
解析:
思路一:递归
终止条件: 当子问题到达叶子节点后,后一个不管左右都是空,因此遇到空节点就返回。
返回值: 每次处理完子问题后,就是将子问题访问过的元素返回,依次存入了数组中。
本级任务: 每个子问题优先访问这棵子树的根节点,然后递归进入左子树和右子树。
具体做法:
step 1:准备数组用来记录遍历到的节点值,Java可以用List,C++可以直接用vector。
step 2:从根节点开始进入递归,遇到空节点就返回,否则将该节点值加入数组。
step 3:依次进入左右子树进行递归。
import java.util.*;
public class Solution {
public void preorder(List<Integer> list, TreeNode root){
//遇到空节点则返回
if(root == null)
return;
//先遍历根节点
list.add(root.val);
//再去左子树
preorder(list, root.left);
//最后去右子树
preorder(list, root.right);
}
public int[] preorderTraversal (TreeNode root) {
//添加遍历结果的数组
List<Integer> list = new ArrayList();
//递归前序遍历
preorder(list, root);
//返回的结果
int[] res = new int[list.size()];
for(int i = 0; i < list.size(); i++)
res[i] = list.get(i);
return res;
}
}
思路二:栈
step 1:优先判断树是否为空,空树不遍历。
step 2:准备辅助栈,首先记录根节点。
step 3:每次从栈中弹出一个元素,进行访问,然后验证该节点的左右子节点是否存在,存的话的加入栈中,优先加入右节点。
import java.util.*;
public class Solution {
public int[] preorderTraversal (TreeNode root) {
//添加遍历结果的数组
List<Integer> list = new ArrayList();
Stack<TreeNode> s = new Stack<TreeNode>();
//空树返回空数组
if(root == null)
return new int[0];
//根节点先进栈
s.push(root);
while(!s.isEmpty()){
//每次栈顶就是访问的元素
TreeNode node = s.pop();
list.add(node.val);
//如果右边还有右子节点进栈
if(node.right != null)
s.push(node.right);
//如果左边还有左子节点进栈
if(node.left != null)
s.push(node.left);
}
//返回的结果
int[] res = new int[list.size()];
for(int i = 0; i < list.size(); i++)
res[i] = list.get(i);
return res;
}
}
4.BM24 二叉树的中序遍历
题目:
给定一个二叉树的根节点root,返回它的中序遍历结果。
数据范围:树上节点数满足0≤n≤1000,树上每个节点的值满足0≤val≤1000
进阶:空间复杂度 O(n),时间复杂度 O(n)
解析:
思路一:递归
终止条件: 当子问题到达叶子节点后,后一个不管左右都是空,因此遇到空节点就返回。
返回值: 每次处理完子问题后,就是将子问题访问过的元素返回,依次存入了数组中。
本级任务: 每个子问题优先访问左子树的子问题,等到左子树的结果返回后,再访问自己的根节点,然后进入右子树。
具体做法:
step 1:准备数组用来记录遍历到的节点值,Java可以用List,C++可以直接用vector。
step 2:从根节点开始进入递归,遇到空节点就返回,否则优先进入左子树进行递归访问。
step 3:左子树访问完毕再回到根节点访问。
step 4:最后进入根节点的右子树进行递归。
import java.util.*;
public class Solution {
public void inorder(List<Integer> list, TreeNode root){
//遇到空节点则返回
if(root == null)
return;
//先去左子树
inorder(list, root.left);
//再访问根节点
list.add(root.val);
//最后去右子树
inorder(list, root.right);
}
public int[] inorderTraversal (TreeNode root) {
//添加遍历结果的数组
List<Integer> list = new ArrayList();
//递归中序遍历
inorder(list, root);
//返回的结果
int[] res = new int[list.size()];
for(int i = 0; i < list.size(); i++)
res[i] = list.get(i);
return res;
}
}
思路二:栈
step 1:优先判断树是否为空,空树不遍历。
step 2:准备辅助栈,当二叉树节点为空了且栈中没有节点了,我们就停止访问。
step 3:从根节点开始,每次优先进入每棵的子树的最左边一个节点,我们将其不断加入栈中,用来保存父问题。
step 4:到达最左后,可以开始访问,如果它还有右节点,则将右边也加入栈中,之后右子树的访问也是优先到最左。
import java.util.*;
public class Solution {
public int[] inorderTraversal (TreeNode root) {
ArrayList<Integer> list = new ArrayList();
Stack<TreeNode> s = new Stack<TreeNode>();
if (root == null)
return new int[0];
//当树节点不为空或栈中有节点时
while ( root != null || !s.isEmpty()) {
while ( root != null) {
s.push(root);
root = root.left;
}
//访问该节点
TreeNode node = s.pop();
list.add(node.val);
//进入右节点
root = node.right;
}
int[] res = new int[list.size()];
for (int i = 0; i < list.size(); i++)
res[i] = list.get(i);
return res;
}
}
5.BM25 二叉树的后序遍历
题目:
略
解析:
思路一:递归
终止条件: 当子问题到达叶子节点后,后一个不管左右都是空,因此遇到空节点就返回。
返回值: 每次处理完子问题后,就是将子问题访问过的元素返回,依次存入了数组中。
本级任务: 对于每个子问题,优先进入左子树的子问题,访问完了再进入右子树的子问题,最后回到父问题访问根节点。
具体做法:
step 1:准备数组用来记录遍历到的节点值,Java可以用List,C++可以直接用vector。
step 2:从根节点开始进入递归,遇到空节点就返回,否则优先进入左子树进行递归访问。
step 3:左子树访问完毕再进入根节点的右子树递归访问。
step 4:最后回到根节点,访问该节点。
import java.util.*;
public class Solution {
public void postorder(List<Integer> list, TreeNode root){
//遇到空节点则返回
if(root == null)
return;
//先去左子树
postorder(list, root.left);
//再去右子树
postorder(list, root.right);
//最后访问根节点
list.add(root.val);
}
public int[] postorderTraversal (TreeNode root) {
//添加遍历结果的数组
List<Integer> list = new ArrayList();
//递归后序遍历
postorder(list, root);
//返回的结果
int[] res = new int[list.size()];
for(int i = 0; i < list.size(); i++)
res[i] = list.get(i);
return res;
}
}
思路二:栈
step 1:开辟一个辅助栈,用于记录要访问的子节点,开辟一个前序指针pre。
step 2:从根节点开始,每次优先进入每棵的子树的最左边一个节点,我们将其不断加入栈中,用来保存父问题。
step 3:弹出一个栈元素,看成该子树的根,判断这个根的右边有没有节点或是有没有被访问过,如果没有右节点或是被访问过了,可以访问这个根,并将前序节点标记为这个根。
step 4:如果没有被访问,那这个根必须入栈,进入右子树继续访问,只有右子树结束了回到这里才能继续访问根。
import java.util.*;
public class Solution {
public int[] postorderTraversal (TreeNode root) {
//添加遍历结果的数组
List<Integer> list = new ArrayList();
Stack<TreeNode> s = new Stack<TreeNode>();
TreeNode pre = null;
while(root != null || !s.isEmpty()){
//每次先找到最左边的节点
while(root != null){
s.push(root);
root = root.left;
}
//弹出栈顶
TreeNode node = s.pop();
//如果该元素的右边没有或是已经访问过
if(node.right == null || node.right == pre){
//访问中间的节点
list.add(node.val);
//且记录为访问过了
pre = node;
}else{
//该节点入栈
s.push(node);
//先访问右边
root = node.right;
}
}
//返回的结果
int[] res = new int[list.size()];
for(int i = 0; i < list.size(); i++)
res[i] = list.get(i);
return res;
}
}
6. BM26 求二叉树的层序遍历
题目:
解析:
利用队列。
step 1:首先判断二叉树是否为空,空树没有遍历结果。
step 2:建立辅助队列,根节点首先进入队列。不管层次怎么访问,根节点一定是第一个,那它肯定排在队伍的最前面。
step 3:每次进入一层,统计队列中元素的个数。因为每当访问完一层,下一层作为这一层的子节点,一定都加入队列,而再下一层还没有加入,因此此时队列中的元素个数就是这一层的元素个数。
step 4:每次遍历这一层这么多的节点数,将其依次从队列中弹出,然后加入这一行的一维数组中,如果它们有子节点,依次加入队列排队等待访问。
step 5:访问完这一层的元素后,将这个一维数组加入二维数组中,再访问下一层。
import java.util.*;
public class Solution {
public ArrayList<ArrayList<Integer>> levelOrder (TreeNode root) {
ArrayList<ArrayList<Integer>> arr = new ArrayList();
if (root == null) {
return null;
}
Queue<TreeNode> q = new ArrayDeque();
q.add(root);
while ( !q.isEmpty()) {
ArrayList<Integer> row = new ArrayList();
int n = q.size();
for ( int i = 0 ; i < n; i++) {
TreeNode cur = q.poll();
row.add(cur.val);
if (cur.left != null) {
q.add(cur.left);
}
if (cur.right != null) {
q.add(cur.right);
}
}
arr.add(row);
}
return arr;
}
}
7. BM28 二叉树的最大深度
题目:
求给定二叉树的最大深度,
深度是指树的根节点到任一叶子节点路径上节点的数量。
最大深度是所有叶子节点的深度的最大值。
(注:叶子节点是指没有子节点的节点。)
数据范围:0≤n≤100000,树上每个节点的val满足 ∣val∣≤100
要求: 时间复杂度 O(n)
解析:
思路一:
递归
import java.util.*;
public class Solution {
public int maxDepth (TreeNode root) {
if(root == null)
return 0;
return Math.max(maxDepth(root.left), maxDepth(root.right)) + 1;
}
}
思路二:
层次遍历,每遍历完一层,深度加一。
import java.util.*;
public class Solution {
public int maxDepth (TreeNode root) {
if (root == null)
return 0;
Queue<TreeNode> q = new ArrayDeque();
q.offer(root);
int hight = 0;
while (! q.isEmpty()) {
int n = q.size();
for (int i = 0 ; i < n; i++) {
TreeNode node = q.poll();
if (node.left != null) {
q.offer(node.left);
}
if (node.right != null) {
q.offer(node.right);
}
}
hight ++;
}
return hight;
}
}
8. BM30 二叉搜索树与双向链表
题目:
解析:
注意是二叉搜索树的特点。解法就是在中序遍历的基础上加东西。
思路一:递归
import java.util.*;
public class Solution {
public TreeNode head = null;
public TreeNode pre = null;
public TreeNode Convert(TreeNode pRootOfTree) {
if (pRootOfTree == null) {
return null;
}
Convert(pRootOfTree.left);
if (pre == null) {
head = pRootOfTree;
pre = pRootOfTree;
} else {
pre.right = pRootOfTree;
pRootOfTree.left = pre;
pre = pRootOfTree;
}
Convert(pRootOfTree.right);
return head;
}
}
思路二:利用栈
二叉树中序遍历除了递归方法,我们还可以尝试非递归解法,与常规的非递归中序遍历几乎相同,还是借助栈来辅助,只是增加了连接节点。
具体做法:
step 1:创建两个指针,一个指向题目中要求的链表头(head),一个指向当前遍历的前一节点(pre),创建一个布尔型变量,标记是否是第一次到最左,因为第一次到最左就是链表头。
step 2:判断空树不能连接。
step 3:初始化一个栈辅助中序遍历。
step 4:依次将父节点加入栈中,直接进入二叉树最左端。
step 5:第一次进入最左,初始化head与pre,然后进入它的根节点开始连接。
step 6:最后将右子树加入栈中,栈中依次就弹出“左中右”的节点顺序,直到栈为空。
import java.util.*;
public class Solution {
public TreeNode Convert(TreeNode pRootOfTree) {
if (pRootOfTree == null) {
return null;
}
Stack<TreeNode> s = new Stack();
TreeNode head = null;
TreeNode pre = null;
while (!s.isEmpty() || pRootOfTree != null) {
while (pRootOfTree != null) {
s.push(pRootOfTree);
pRootOfTree = pRootOfTree.left;
}
pRootOfTree = s.pop();
if (pre == null) {
head = pRootOfTree;
pre = pRootOfTree;
} else {
pre.right = pRootOfTree;
pRootOfTree.left = pre;
pre = pRootOfTree;
}
pRootOfTree = pRootOfTree.right;
}
return head;
}
}
题目:
解析:
题目:
解析:
题目:
解析:
题目:
解析:
题目:
解析: