目录
- 1. 数组排列
- 2. 找出数组中重复的数字
- 3. 剪绳子(砍竹子 I)
- 4. 砍竹子 II
- 5. 位1的个数
- 6. 数值的整数次方
- 7. 调整数组顺序使奇数位于偶数前面(训练计划 I)
- 8. 树的子结构
- 9. 栈的压入、弹出序列(验证栈序列)
- 10. 二叉搜索树的后序遍历队列(验证二叉搜索树的后序遍历序列)
- 11. 将二叉搜索树转化为排序的双向链表
- 12. 序列化与反序列化二叉树
- 13. 找到第 k 位数字
- 14. 把数组排成最小的数(破解闯关密码)
- 15. 把数字翻译成字符串
- 16. 礼物的最大价值
- 17.第一个只出现一次的字符
- 18. 0~n中缺失的数字
- 19. 数组中数字出现的次数(撞色搭配)
- 20 . 数组中数字出现的次数Ⅱ(训练计划 VI)
- 21. 文件组合(和为s的连续正数序列)
- 22. 队列的最大值(设计自助结算系统)
- 23. 动态口令(旋转字符串)
- 24. 统计结果概率(n个骰子的点数)
- 25. 文物朝代判断(扑克牌中的顺子)
- 26. 求1+2+...+n(设计机械累加器)
- 27. 圆圈中最后剩下的数字(破冰游戏)
- 30. 不用加减乘除做加法(加密运算)
- 31. 二叉搜索树的最近公共祖先
1. 数组排列
输入一组数字(可能包含重复数字),输出其所有的排列方式。
数据范围
输入数组长度 [0,6]。
数组元素取值范围 [1,10]。
输入:[1,2,3]
输出:
[
[1,2,3],
[1,3,2],
[2,1,3],
[2,3,1],
[3,1,2],
[3,2,1]
]
解法一
先对数组进行排序,使得可重复的元素相邻,便于后续跳过重复的递归。之后每层递归从未选过的元素中选出一个元素作为第i个数,采用一个bool数组记录哪些元素已经访问过。由于有重复元素存在,在同一层的递归中,同样大小的元素选了就不能再选第二遍了。因此,前一个与自己相同的元素并且被标注过没被访问过,则说明前一个元素已经遍历过了(因为数组排序过,从小到大遍历),后面恢复自己之前的状态了(回溯),因此当前元素不能作为本层元素。如果前一个元素与自己相同,但是显示已经访问过了,说明前一个元素是用在前面某层递归中了,不属于当前层,于是当前元素可以使用。
class Solution {
boolean[] visited;
public List<List<Integer>> permutation(int[] nums) {
visited = new boolean[nums.length];
Arrays.sort(nums);
List<List<Integer>> result = new ArrayList<>();
List<Integer> list = new ArrayList<>();
arrange(nums,result,0,list);
return result;
}
public void arrange(int[] nums, List<List<Integer>> result, int first, List<Integer> list){
if(first == nums.length){
result.add(new ArrayList<Integer>(list));
return;
}
for(int i = 0; i < nums.length; i++){
if(visited[i] || (i > 0 && nums[i] == nums[i-1] && !visited[i-1])) continue;
list.add(nums[i]);
visited[i] = true;
arrange(nums,result,first+1,list);
visited[i] = false;
list.remove(first);
}
}
}
2. 找出数组中重复的数字
给定一个长度为 n
的整数数组 nums,数组中所有的数字都在 0∼n−1
的范围内。
数组中某些数字是重复的,但不知道有几个数字重复了,也不知道每个数字重复了几次。
请找出数组中任意一个重复的数字。
注意:如果某些数字不在 0∼n−1
的范围内,或数组中不包含重复数字,则返回 -1;
数据范围
0≤n≤1000
样例
给定 nums = [2, 3, 5, 4, 3, 2, 6, 7]。
返回 2 或 3。
解法一
本题可以直接用桶排序,将第一个计数大于1的索引返回即可。但是本题有许多细节需要考虑,例如元素可能大于数组长度n,元素中可能有负数,需要额外考虑。
class Solution {
public int duplicateInArray(int[] nums) {
int n = nums.length;
int[] bucket = new int[nums.length + 1];
int flag = 0;
for(int num : nums){
if(num < 0 || num >= n ) {
return -1;
}
}
for(int num : nums){
bucket[num]++;
if(bucket[num] > 1) return num;
}
return -1;
}
}
解法二
用hashmap记录出现频次:
class Solution {
public int duplicateInArray(int[] nums) {
int n = nums.length;
int result = -1;
HashMap<Integer,Integer> map = new HashMap<>();
int flag = 0;
for(int num : nums){
if(num < 0 || num >= n ) {
return -1;
}
if(map.containsKey(num)){
result = num;
}else{
map.put(num,1);
}
}
return result;
}
}
3. 剪绳子(砍竹子 I)
现需要将一根长为正整数 bamboo_len 的竹子砍为若干段,每段长度均为正整数。请返回每段竹子长度的最大乘积是多少。
解法一
动态规划:dp【i】为长度为i的竹子分段后最大的乘积。
根据规律可以得知,注意1,2,3不应该再分,再分只会变小。因此再将更大的数进行拆分时,拆到剩余3或2就可以停止了。如果继续拆只会变小。并且尽量拆成多个 3相乘会使得乘积更大。如果拆到最后只剩4,再拆成31就不如4大,也许进行停止拆分
因此递推公式:dp[i] = max(2dp[i-2],3*dp[i-3]);
class Solution {
public int cuttingBamboo(int bamboo_len) {
int[] dp = new int[bamboo_len+1];
dp[0] = 1;
dp[1] = 1;
dp[2] = 2;
dp[3] = 3;
if(bamboo_len <= 3) return dp[bamboo_len]-1;
for(int i = 4; i <= bamboo_len; i++){
dp[i] = Math.max(2*dp[i-2],3*dp[i-3]);
}
return dp[bamboo_len];
}
}
解法二
根据上面提到的规律,可以循环拆分出一个个3,然后最后可能剩余1或2或0.
- 剩余0时,直接返回结果即可;
- 剩余1时,需要退回上一个3,并将其合并为4,然后返回结果;
- 剩余2时,直接乘以2返回结果。
class Solution {
public int cuttingRope(int bamboo_len) {
if (bamboo_len <= 3) {
return bamboo_len - 1;
}
int quotient = bamboo_len / 3;
int remainder = bamboo_len % 3;
if (remainder == 0) {
return (int) Math.pow(3, quotient);
} else if (remainder == 1) {
return (int) Math.pow(3, quotient - 1) * 4;
} else {
return (int) Math.pow(3, quotient) * 2;
}
}
}
4. 砍竹子 II
现需要将一根长为正整数 bamboo_len 的竹子砍为若干段,每段长度均为 正整数。请返回每段竹子长度的 最大乘积 是多少。
答案需要取模 1e9+7(1000000007),如计算初始结果为:1000000008,请返回 1。
解法一
思路和上题一直,不过需要保证数不溢出,及时取模。里面会用到一个同余定理:
同余式相加:若a≡b(mod m),c≡d(mod m),则a+c≡(b+d)(mod m);
同余式相乘:若a≡b(mod m),c≡d(mod m),则ac≡(bd)(mod m)。
为了防止结果值溢出,不能再使用pow函数,应当循环一个一个乘以3,然后对1000000007取模
class Solution {
public int cuttingBamboo(int bamboo_len) {
if(bamboo_len <= 3) return bamboo_len -1;
int rem = bamboo_len / 3;
int leave = bamboo_len % 3;
long result = 1;
int p = 1000000007;
for(int i = 1 ; i < rem; i++){
result = 3*result % p;
}
if(leave == 0) return (int)(result*3%p);
else if( leave == 1) return (int)(result*4%p);
return (int)(result*3*2%p);
}
}
5. 位1的个数
编写一个函数,获取一个正整数的二进制形式并返回其二进制表达式中 设置位(位为1) 的个数(也被称为汉明重量)。
解法一
通过与操作(1右移一位,两位,三位…),获取数字的每一位,为1则数量加一。
public class Solution {
// you need to treat n as an unsigned value
public int hammingWeight(int n) {
int count = 0;
for(int i = 0; i < 32;i++){
if(((1<<i) & n) != 0){
count++;
}
}
return count;
}
}
6. 数值的整数次方
实现 pow(x, n) ,即计算 x 的整数 n 次幂函数(即,xn )。
解法一
快速幂:常规的计算幂递归,递归一次幂减一,如果幂次数太大,容易栈溢出。为了减少递归深度,可以选择将幂次进行拆分,实现结果复用。例如x的四次方,就只需要两个x的平方相乘即可,对于x的64次方,普通递归需要64+1次,只需要两个x的32次方,4个16次方,8个8次方,16个4次方,32个2次方,64个一次方组成,递归深度只有6+1层。因此,每次将幂对半拆开,如果n能被2整除,则结果为powpow;如果不能则为xpow*pow。
class Solution {
public double myPow(double x, int n) {
return n > 0 ? pow(x,n) : 1.0/pow(x,-n);
}
public double pow(double x, int n){
if(n == 0) return 1.0;
double num = pow(x,n/2);//重点,计算一次即可
return n % 2 == 0 ? num*num : x*num*num; //重点
}
}
7. 调整数组顺序使奇数位于偶数前面(训练计划 I)
教练使用整数数组 actions 记录一系列核心肌群训练项目编号。为增强训练趣味性,需要将所有奇数编号训练项目调整至偶数编号训练项目之前。请将调整后的训练项目编号以 数组 形式返回。
解法一
双指针法:左右指针分别指向头尾,左指针找到第一个偶数,右指针找到第一个奇数,两者交换。之后左指针继续寻找下一个偶数,右指针继续寻找下一个奇数。。。。
class Solution {
public int[] trainingPlan(int[] actions) {
int i = 0, j = actions.length - 1, tmp;
while(i < j) {
while(i < j && (actions[i] & 1) == 1) i++;
while(i < j && (actions[j] & 1) == 0) j--;
tmp = actions[i];
actions[i] = actions[j];
actions[j] = tmp;
}
return actions;
}
}
8. 树的子结构
给定两棵二叉树 tree1 和 tree2,判断 tree2 是否以 tree1 的某个节点为根的子树具有 相同的结构和节点值 。
注意,空树 不会是以 tree1 的某个节点为根的子树具有 相同的结构和节点值 。
解法一
由于是判断树的子结构是否相同,A中的所有节点都有可能作为子根节点与B结构相同。因此思路就是遍历A的所有节点,然后从每个节点出发判断是否与B结构相同。
class Solution {
public boolean isSubStructure(TreeNode A, TreeNode B) {
if(B == null || A == null) return false;
//相当于前序遍历,检查根,然后左,右
return recur(A,B) || isSubStructure(A.left,B) || isSubStructure(A.right,B);
}
public boolean recur(TreeNode A, TreeNode B){
if(B == null) return true;
if(A == null || A.val != B.val) return false;
return recur(A.left,B.left) && recur(A.right,B.right);
}
}
9. 栈的压入、弹出序列(验证栈序列)
给定 pushed 和 popped 两个序列,每个序列中的 值都不重复,只有当它们可能是在最初空栈上进行的推入 push 和弹出 pop 操作序列的结果时,返回 true;否则,返回 false 。
解法一
遍历数组,依次入栈,每个元素入栈后就检查是否与popped数组中元素相同(可能连续几个元素都相同,因此需要用while),相同则弹出,然后popped待弹出元素指针后移。最后如果栈不为空,则说明元素弹不出去,序列不满足弹出规则。
class Solution {
public boolean validateStackSequences(int[] pushed, int[] popped) {
Deque<Integer> stack = new ArrayDeque<Integer>();//效率比stack高
// Stack<Integer> stack = new Stack<>();//内部继承于Vector,线程安全,要加锁
int p = 0;
for(int i = 0 ; i < pushed.length; i++){
stack.push(pushed[i]);
while(!stack.isEmpty() && stack.peek() == popped[p]){
stack.pop();
p++;
}
}
return stack.isEmpty() ? true: false;
}
}
10. 二叉搜索树的后序遍历队列(验证二叉搜索树的后序遍历序列)
请实现一个函数来判断整数数组 postorder 是否为二叉搜索树的后序遍历结果。
解法一
先解读题意:题目给一个数组,判断它是不是二叉搜索树的后序遍历结果。换而言之就是,数组是一个二叉树的后序遍历结果,判断它能否构建成二叉搜索树。
根据后序遍历特性,序列的分布为左子树、右子树、根节点,以及二叉搜索树中,左子树的所有节点小于根节点小于右子树的所有节点,可以对后序遍历数组进行递归分解,每次将当前树分解为根节点,左子树,右子树,判断是否能够将一棵树的所有节点进行完整的分割。若不能则返回false。
class Solution {
public boolean verifyTreeOrder(int[] postorder) {
return recur(postorder,0,postorder.length-1);
}
public boolean recur(int[] postorder,int i , int j){
if(i >= j) return true;//注意,有可能原来的区间中没有元素,例如没有小于rootval的,之后recur(postorder,i,m-1),此时i > m-1,如果不返回,后续就会越界
int rootVal = postorder[j];
int p = i;
while(postorder[p] < rootVal){//找出左子树集合区间
p++;
}
int m = p;
while(postorder[p] > rootVal){//找出右子树集合区间
p++;
}
//如果p不等于j说明无法完整分割当前这棵树的所有节点,则说明不满足二叉搜索树的特性
return p == j && recur(postorder,i,m-1) && recur(postorder,m,j-1);
}
}
11. 将二叉搜索树转化为排序的双向链表
将一个 二叉搜索树 就地转化为一个 已排序的双向循环链表 。
对于双向循环列表,你可以将左右孩子指针作为双向循环链表的前驱和后继指针,第一个节点的前驱是最后一个节点,最后一个节点的后继是第一个节点。
特别地,我们希望可以 就地 完成转换操作。当转化完成以后,树中节点的左指针需要指向前驱,树中节点的右指针需要指向后继。还需要返回链表中最小元素的指针。
解法一
二叉搜索树的中序遍历结果就是排序后的序列,因此可以在中序遍历过程中,记录上一个遍历到的节点,然后在处理当前节点时,将当前节点的前驱设置为上一个节点,再将上一个节点的后继设置为当前节点。如果上一个节点为null,说明当前节点是链表的头结点,需要单独记录。链表构建完后,还需要将链表头尾相连。
class Solution {
Node pre;
Node head;
public Node treeToDoublyList(Node root) {
if(root == null) return null;
dfs(root);
head.left = pre;//收尾相连
pre.right = head;
return head;
}
public void dfs(Node root){
if(root == null) return;
dfs(root.left);
if(pre == null){//说明不存在前一个节点,即为链表开头
head = root;
} else{//说明有前一个节点,则设置当前节点的前驱与上一个节点的后继
root.left = pre;
pre.right = root;
}
pre = root;//在递归搜索右节点时,按照中序遍历的思想,根节点为右子树的前驱节点
dfs(root.right);
}
}
12. 序列化与反序列化二叉树
请设计一个算法来实现二叉树的序列化与反序列化。这里不限定你的序列 / 反序列化算法执行逻辑,你只需要保证一个二叉树可以被序列化为一个字符串并且将这个字符串反序列化为原始的树结构。(只需要保证序列化能反序列化回去即可,不用必须按照示例输出结果)
解法一
迭代法:通过层序遍历(通过队列实现),将节点一层一层序列化和反序列化。需要注意,序列化过程中,对于null节点需不需要继续为其创建null的子节点信息,看个人选择,本方法选择不继续创建。也就是说,null节点不再继续入队,到此为止。后续反序列化的时候,先对序列化数据进行分割,每次出队一个节点,然后一轮中向分割后的数组中连续访问两个节点位置的数据,如果不为null才入队,也就保证了只有有数据的节点才会继续创建节点,没有数据的节点到此为止。
public class Codec {
public String serialize(TreeNode root) {
if(root == null) return "[]";
StringBuilder res = new StringBuilder("[");
Queue<TreeNode> queue = new LinkedList<>();
queue.add(root);
while(!queue.isEmpty()) {
TreeNode node = queue.poll();
if(node != null){
queue.add(node.left);
queue.add(node.right);
res.append(node.val);
}else{
res.append("null");
}
res.append(',');
}
res.deleteCharAt(res.length() - 1);
res.append("]");
return res.toString();
}
public TreeNode deserialize(String data) {
if(data.equals("[]")) return null;
String newdata = data.substring(1,data.length()-1);
String[] sarr = newdata.split(",");
Queue<TreeNode> queue = new LinkedList<>();
TreeNode root = new TreeNode(Integer.parseInt(sarr[0]));
int index = 1;
queue.add(root);
while(!queue.isEmpty()){
TreeNode p = queue.poll();
if(!sarr[index].equals("null")){
p.left = new TreeNode(Integer.parseInt(sarr[index]));
queue.add(p.left);
}
index++;
if(!sarr[index].equals("null")){
p.right = new TreeNode(Integer.parseInt(sarr[index]));
queue.add(p.right);
}
index++;
}
return root;
}
}
解法二
递归:前序遍历存储节点信息。按照前序遍历顺序依次存储节点信息进行序列化。反序列化时也是采用前序遍历的方式生成,用一个index指针指向序列化后的数组元素,递归生成一个节点,则index++,遇到null则递归返回,index也要++。
public class Codec {
public String serialize(TreeNode root) {
if(root == null) return "[]";
StringBuilder s = new StringBuilder("[");
rserialize(root,s);
s.deleteCharAt(s.length()-1);
s.append(']');
return s.toString();
}
public void rserialize(TreeNode root, StringBuilder s){
if(root == null){
s.append("null,");
return;
}
s.append(root.val).append(',');
rserialize(root.left,s);
rserialize(root.right,s);
}
int index = 0;
public TreeNode deserialize(String data) {
if(data.equals("[]")) return null;
String newdata = data.substring(1,data.length()-1);
String[] sarr = newdata.split(",");
return rdeserialize(sarr);
}
public TreeNode rdeserialize(String[] sarr){
if(sarr[index].equals("null")){
index++;
return null;
}
TreeNode root = new TreeNode(Integer.parseInt(sarr[index]));
index++;
root.left = rdeserialize(sarr);
root.right = rdeserialize(sarr);
return root;
}
}
13. 找到第 k 位数字
某班级学号记录系统发生错乱,原整数学号序列 [0,1,2,3,4,…] 分隔符丢失后变为 01234… 的字符序列。请实现一个函数返回该字符序列中的第 k 位数字。(从数字1开始)
解法一
找规律:首先根据k的大小,确定第k位所在的数字是几位数。之后从该几位数的最小值开始搜索,10000,10001.。。。。。
class Solution {
public int findKthNumber(int k) {
int i = 1;
long count = 9;
long start = 1;
while(k > count){
k -= count;
start *= 10;
i++;
count = 9*start*i;
}
long num = start + (k-1)/i;
return Long.toString(num).charAt((k-1) % i) - '0';
}
}
14. 把数组排成最小的数(破解闯关密码)
闯关游戏需要破解一组密码,闯关组给出的有关密码的线索是:
一个拥有密码所有元素的非负整数数组 password
密码是 password 中所有元素拼接后得到的最小的一个数
请编写一个程序返回这个密码。
解法一
重点需要想出:通过比较字符串s1与s2是s1+s2拼接后更大还是s2+s1拼接后更大。按照这个规律,保证数组前面都是往前拼能够得到最小的数。从而进行一次排序即可
注意!Array.sort()如果要使用自定义排序的话,数组元素必须是对象类型,不能是基本类型。
class Solution {
public String crackPassword(int[] password) {
Integer[] arr = new Integer[password.length];
for(int i = 0; i < password.length; i++){
arr[i] = password[i];
}
Arrays.sort(arr,(a,b)->{
String as = Integer.toString(a);
String bs = Integer.toString(b);
return (as+bs).compareTo(bs+as);
});
StringBuilder sb = new StringBuilder();
for(int num : arr){
sb.append(num);
}
return sb.toString();
}
}
15. 把数字翻译成字符串
现有一串神秘的密文 ciphertext,经调查,密文的特点和规则如下:
密文由非负整数组成
数字 0-25 分别对应字母 a-z
请根据上述规则将密文 ciphertext 解密为字母,并返回共有多少种解密结果。
解法一
该题很容易想到动态规划。dp【i】 = dp[i-1] + dp [i-2];但是dp [i-2]的前提是i大于等于2且最后两位数字组成的数必须大于等于10小于等于25,否则只能dp【i】 = dp[i-1] 。由于题目说了全是非负整数组成,因此‘06’这种整数不存在。
class Solution {
public int crackNumber(int ciphertext) {
String s = Integer.toString(ciphertext);
int len = s.length();
if(len == 1) return 1;
int[] dp = new int[len];
dp[0] = 1;
dp[1] = 1;
if(Integer.parseInt(s.substring(0,2)) <= 25 && Integer.parseInt(s.substring(0,2)) >= 10){
dp[1]++;
}
int result = dp[1];
for(int i = 2; i < len; i++){
if(Integer.parseInt(s.substring(i-1,i+1)) <= 25 && Integer.parseInt(s.substring(i-1,i+1)) >= 10 ){
dp[i] = dp[i-2] + dp[i-1];
}else{
dp[i] = dp[i-1];
}
result = Math.max(result,dp[i]);
}
return result;
}
}
16. 礼物的最大价值
现有一个记作二维矩阵 frame 的珠宝架,其中 frame[i][j] 为该位置珠宝的价值。拿取珠宝的规则为:
只能从架子的左上角开始拿珠宝
每次可以移动到右侧或下侧的相邻位置
到达珠宝架子的右下角时,停止拿取
注意:珠宝的价值都是大于 0 的。除非这个架子上没有任何珠宝,比如 frame = [[0]]。
解法一
动态规划:递推公式:dp[i][j] = Math.max(dp[i-1][j],dp[i][j-1]) + frame[i-1][j-1];
class Solution {
public int jewelleryValue(int[][] frame) {
int n = frame.length;
int m = frame[0].length;
int[][] dp = new int[n+1][m+1];
for(int i = 0; i <= n; i++){
for(int j = 0; j <= m; j++){
dp[i][j] = 0;
}
}
for(int i = 1; i <= n; i++){
for(int j = 1; j <= m; j++){
dp[i][j] = Math.max(dp[i-1][j],dp[i][j-1]) + frame[i-1][j-1];
}
}
return dp[n][m];
}
}
一维数组
class Solution {
public int jewelleryValue(int[][] frame) {
int n = frame.length;
int m = frame[0].length;
int[] dp = new int[m+1];
for(int i = 1; i <= n; i++){
for(int j = 1; j <= m; j++){
dp[j] = Math.max(dp[j],dp[j-1]) + frame[i-1][j-1];
}
}
return dp[m];
}
}
17.第一个只出现一次的字符
某套连招动作记作仅由小写字母组成的序列 arr,其中 arr[i] 第 i 个招式的名字。请返回第一个只出现一次的招式名称,如不存在请返回空格。
解法一
遍历两次,第一个统计各字符出现次数,第二次找到第一个出现次数为1的字符
class Solution {
public char dismantlingAction(String arr) {
int[] array = new int[26];
for(int i = 0; i < arr.length();i++){
array[arr.charAt(i)-'a']++;
}
for(int i = 0; i < arr.length();i++){
if(array[arr.charAt(i)-'a'] == 1){
return arr.charAt(i);
}
}
return ' ';
}
}
18. 0~n中缺失的数字
某班级 n 位同学的学号为 0 ~ n-1。点名结果记录于升序数组 records。假定仅有一位同学缺席,请返回他的学号。
解法一
遍历一边,下标与值不等则返回。
class Solution {
public int takeAttendance(int[] records) {
for(int i =0;i < records.length;i++){
if(records[i] != i){
return i;
}
}
return records[records.length-1]+1;
}
}
解法二
二分法:每次分半,如果mid与数组mid位置的元素相同,则说明确实元素在右边,否则在左边。至于边界问题需要考虑,如果两个指针相等,判断mid与records[mid],如果相等,则说明最终结果值在右侧,如果不等,说明当前mid就是结果。所以可以写出,这样结果始终是low。
if(records[mid] == mid){
low = mid + 1;
}else{
high = mid - 1;
}
class Solution {
public int takeAttendance(int[] records) {
int len = records.length;
int low = 0;
int high = len - 1;
while(low <= high){
int mid = low + (high-low)/2;
if(records[mid] == mid){
low = mid + 1;
}else{
high = mid - 1;
}
}
return low;
}
}
19. 数组中数字出现的次数(撞色搭配)
整数数组 sockets 记录了一个袜子礼盒的颜色分布情况,其中 sockets[i] 表示该袜子的颜色编号。礼盒中除了一款撞色搭配的袜子,每种颜色的袜子均有两只。请设计一个程序,在时间复杂度 O(n),空间复杂度O(1) 内找到这双撞色搭配袜子的两个颜色编号。
解法一
假设撞色袜子分别为a,b。由异或的运算规律我们可以得知,所以元素异或后,结果等于a异或b。为了分别获得这两个元素,可以考虑将原本的元素进行分组,保证一组中有a,另一组中有b,且组内其他袜子都成对。还是根据异或规则可知,相同位异或为0,不同位异或为1.如果找到a异或b的结果中一个1,则说明两个数原本在该位上的值不同,因此可以按照这一位将数组中的元素分成两组,然后分别异或,最终可以得到a和b。
class Solution {
public int[] sockCollocation(int[] sockets) {
int midresult = 0;
for(int num : sockets){
midresult ^= num;
}
int i = 1;
while((i & midresult) == 0){
i <<= 1;
}
int a = 0, b = 0;
for(int num : sockets){
if((i & num) != 0){
a ^= num;
}else{
b ^= num;
}
}
return new int[]{a,b};
}
}
20 . 数组中数字出现的次数Ⅱ(训练计划 VI)
教学过程中,教练示范一次,学员跟做三次。该过程被混乱剪辑后,记录于数组 actions,其中 actions[i] 表示做出该动作的人员编号。请返回教练的编号。
解法一
用哈希表统计,然后取出数量为1的key。
解法二
用有限状态机,相当于统计每个元素每一位上1的个数,对3取余后就得到最终只出现一次的数了。由于二进制只有01,因此需要用到数电中的状态转换表,求出状态转换表达式。
class Solution {
public int trainingPlan(int[] actions) {
int ones = 0, twos = 0;
for(int action : actions){
ones = ones ^ action & ~twos;
twos = twos ^ action & ~ones;
}
return ones;
}
}
21. 文件组合(和为s的连续正数序列)
待传输文件被切分成多个部分,按照原排列顺序,每部分文件编号均为一个 正整数(至少含有两个文件)。传输要求为:连续文件编号总和为接收方指定数字 target 的所有文件。请返回所有符合该要求的文件传输组合列表。
注意,返回时需遵循以下规则:
每种组合按照文件编号 升序 排列;
不同组合按照第一个文件编号 升序 排列。
解法一
类似于滑动窗口,大了就缩,小了就往前扩
class Solution {
public int[][] fileCombination(int target) {
List<Integer> list = new LinkedList<>();
LinkedList<int[]> resultList = new LinkedList<>();
int i = 1;
int sum = 0;
while(i < target){
sum += i;
list.addLast(i);
if(sum > target){
while(sum > target){
int start = list.getFirst();
sum -= start;
list.removeFirst();
}
}
if(sum == target){
resultList.add(list.stream().mapToInt(Integer::intValue).toArray());
}
i++;
}
return resultList.toArray(new int[resultList.size()][]);
}
}
解法二
效率更高,在集合操作上要更省时间
class Solution {
public int[][] fileCombination(int target) {
List<int[]> vec = new ArrayList<int[]>();
for (int l = 1, r = 2; l < r;) {
int sum = (l + r) * (r - l + 1) / 2;
if (sum == target) {
int[] res = new int[r - l + 1];
for (int i = l; i <= r; ++i) {
res[i - l] = i;
}
vec.add(res);
l++;
} else if (sum < target) {
r++;
} else {
l++;
}
}
return vec.toArray(new int[vec.size()][]);
}
}
22. 队列的最大值(设计自助结算系统)
请设计一个自助结账系统,该系统需要通过一个队列来模拟顾客通过购物车的结算过程,需要实现的功能有:
get_max():获取结算商品中的最高价格,如果队列为空,则返回 -1
add(value):将价格为 value 的商品加入待结算商品队列的尾部
remove():移除第一个待结算的商品价格,如果队列为空,则返回 -1
注意,为保证该系统运转高效性,以上函数的均摊时间复杂度均为 O(1)
解法一
维护两个队列,一个用来正常出队入队。另一个队列用来保存一个递减队列。当1112,当2入队时,前面的1都可以作废了,因为只要有2在,队列中最大值就不可能是1了,直到2出队。每次一个元素入队,在单调递减队列中判断队尾元素是否比它小,如果是,则说明新的value可以进入队列后,前面比它小的元素都可以作废了,直接出队,然后将value放入递减队列队尾
class Checkout {
LinkedList<Integer> queue = new LinkedList();
LinkedList<Integer> maxqueue = new LinkedList();
public Checkout() {
}
public int get_max() {
if(maxqueue.isEmpty()) return -1;
return maxqueue.getFirst();
}
public void add(int value) {
while(!maxqueue.isEmpty() && maxqueue.getLast() < value){
maxqueue.removeLast();
}
maxqueue.addLast(value);
queue.addLast(value);
}
public int remove() {
if(queue.isEmpty()) return -1;
// int ans = queue.poll();
int ans = queue.pollFirst();
if (ans == maxqueue.getFirst()) {//注意因为队列peek()或者peekFirst()返回的对象不是整型,而是Object,所以不能用==,而应该用equals。或者先转换为int再用等号比较
maxqueue.removeFirst();
}
return ans;
}
}
23. 动态口令(旋转字符串)
某公司门禁密码使用动态口令技术。初始密码为字符串 password,密码更新均遵循以下步骤:
设定一个正整数目标值 target
将 password 前 target 个字符按原顺序移动至字符串末尾
请返回更新后的密码字符串。
解法一
剪切出两个子串,重新拼接。原地算法可以先反转左边的子串,再反转右边的子串,最后反转整个子串
class Solution {
public String dynamicPassword(String password, int target) {
int len = password.length();
String behind = password.substring(target,len);
String before = password.substring(0,target);
return behind+before;
}
}
//c++
class Solution {
public:
string reverseLeftWords(string s, int n) {
reverse(s.begin(), s.begin() + n);
reverse(s.begin() + n, s.end());
reverse(s.begin(), s.end());
return s;
}
};
24. 统计结果概率(n个骰子的点数)
你选择掷出 num 个色子,请返回所有点数总和的概率。
你需要用一个浮点数数组返回答案,其中第 i 个元素代表这 num 个骰子所能掷出的点数集合中第 i 小的那个的概率。
解法一
动态规划:可以先想一个筛子各点的概率,然后在一个筛子的基础上再加一个筛子,概率分别是多少,如此递推即可。最后可以得到递推公式,dp[ i ][ n ] = 1/6.0*(dp[i - 1][ n - 1]+ dp[i - 1][ n - 2]+ dp[i - 1][ n - 3]+。。。。+ dp[i - 1][ n - 6])。相当于上一个数量的筛子所能构成的所有结果,搭配新加的筛子,新筛子为1-6的情况下,dp需要如何修改。
下面代码中,相当于遍历旧若干个筛子的所有情况,每次在旧情况上+1,+2,,,,会分发在新的dp对应的不同六个位置上。
class Solution {
public double[] statisticsProbability(int num) {
double[] dp = new double[6];
Arrays.fill(dp,1.0/6.0);
for(int i = 2; i <= num;i++){
double[] tmp = new double[5*i+1];
for(int j = 0; j < dp.length; j++){
for(int k = 0; k < 6; k++){
tmp[j+k] += dp[j+k-k]/6.0;
}
}
dp = tmp;
}
return dp;
}
}
25. 文物朝代判断(扑克牌中的顺子)
展览馆展出来自 13 个朝代的文物,每排展柜展出 5 个文物。某排文物的摆放情况记录于数组 places,其中 places[i] 表示处于第 i 位文物的所属朝代编号。其中,编号为 0 的朝代表示未知朝代。请判断并返回这排文物的所属朝代编号是否能够视为连续的五个朝代(如遇未知朝代可算作连续情况)。
解法一
class Solution {
public boolean checkDynasty(int[] places) {
HashSet<Integer> set = new HashSet<>();
int count = 0;
int min = 14;
for(int num : places){
if(num == 0){
count++;
}else{
set.add(num);
min = Math.min(min,num);
}
}
for(int i = 1; i < 5; i++){
min = min + 1;
if(!set.contains(min)){
if(count <= 0){
return false;
}
count--;
}
}
return true;
}
}
class Solution {
public boolean checkDynasty(int[] places) {
Set<Integer> repeat = new HashSet<>();
int max = 0, min = 14;
for(int place : places) {
if(place == 0) continue; // 跳过未知朝代
max = Math.max(max, place); // 最大编号朝代
min = Math.min(min, place); // 最小编号朝代
if(repeat.contains(place)) return false; // 若有重复,提前返回 false
repeat.add(place); // 添加此朝代至 Set
}
return max - min < 5; // 最大编号朝代 - 最小编号朝代 < 5 则连续
}
}
26. 求1+2+…+n(设计机械累加器)
请设计一个机械累加器,计算从 1、2… 一直累加到目标数值 target 的总和。注意这是一个只能进行加法操作的程序,不具备乘除、if-else、switch-case、for 循环、while 循环,及条件判断语句等高级功能。
解法一
递归+借用比较操作符。
class Solution {
public int mechanicalAccumulator(int target) {
return add(target);
}
public int add(int target){
boolean a = target > 0 && (target += add(target - 1)) > 0;
return target;
}
}
27. 圆圈中最后剩下的数字(破冰游戏)
社团共有 num 位成员参与破冰游戏,编号为 0 ~ num-1。成员们按照编号顺序围绕圆桌而坐。社长抽取一个数字 target,从 0 号成员起开始计数,排在第 target 位的成员离开圆桌,且成员离开后从下一个成员开始计数。请返回游戏结束时最后一位成员的编号。
解法一
我们已知最后活下来的是 i,定义每次删掉的位置为 index处, 定义数组长度为 size
0,1,2,3,4,5,6... i ... n-1 第一轮
每一次删除第 m 个数之后,指针开始从 m+1 这个位置为起点,将前 m-1 个数移到数组末尾以供循环
3,4,5,6... i ... n-1,0,1 第二轮
6, 7,8,... i ... n-1,3,4 第三轮
......
i-1,i 第n-1轮
最后仅剩下 i,且此时index下标为 0
规律:每一轮删掉第 m 个数都是将前 m-1 个数移到数组末尾以供循环,每次 第 i 个数相当于左移了 m 次
追本溯源:从最后一次 index 为 0 开始溯源,依次让 index 右移 m 位,即加上 m
倒序每一次 index + m 就能找到上一轮那个 i 所在的 index,同时防止越界要取余该轮数组 size
class Solution {
public int iceBreakingGame(int num, int target) {
return f(num,target);
}
public int f(int num ,int target){
if(num == 1){//只有一个元素时,返回的编号为0
return 0;
}
int x = f(num - 1, target);
return (target % num + x ) % num;
}
}
30. 不用加减乘除做加法(加密运算)
解法一
位运算:将dataA+dataB分解为进位和+非进位和,这样就又是加法,迭代“加”操作,直至进位和为0,则说明此时非进位和就已经代表了总和了。
class Solution {
public int encryptionCalculate(int dataA, int dataB) {
//将dataA+dataB分解为进位和+非进位和,这样就又是加法,
//迭代“加”操作,直至进位和为0,则说明此时非进位和就已经代表了总和了
while(dataB != 0){
int c = (dataA & dataB) << 1;//进位和
dataA ^= dataB;//非进位和
dataB = c;//进位和
}
return dataA;
}
}
31. 二叉搜索树的最近公共祖先
给定一个二叉搜索树, 找到该树中两个指定节点的最近公共祖先。
百度百科中最近公共祖先的定义为:“对于有根树 T 的两个结点 p、q,最近公共祖先表示为一个结点 x,满足 x 是 p、q 的祖先且 x 的深度尽可能大(一个节点也可以是它自己的祖先)。”
说明:
所有节点的值都是唯一的。
p、q 为不同节点且均存在于给定的二叉搜索树中。
例如,给定如下二叉搜索树: root = [6,2,8,0,4,7,9,null,null,3,5]
解法一
可以和之前的二叉树求最近公共祖先一样,详解见leetcode算法刷题笔记(1)第50题。
class Solution {
TreeNode result = null;
public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
findOrigin(root,p,q);
return result;
}
public boolean findOrigin(TreeNode root,TreeNode p,TreeNode q){
if(root == null){
return false;
}
if(result != null) return false;
boolean left = findOrigin(root.left,p,q);
boolean right = findOrigin(root.right,p,q);
boolean cur = root == q || root == p;
if((left && right)||(left && cur) || (right && cur)){
result = root;
}
if(left || right || root == q || root == p){
return true;
}
return false;
}
}
解法二
由于题目新增两个节点一定存在且唯一,则可以依靠二叉搜索树的特性,进行分叉搜索。如果一个节点大于当前root,另一个节点小于当前root,则说明当前root就是它们的最近祖先。
class Solution {
public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
TreeNode ancestor = root;
while (true) {
if (p.val < ancestor.val && q.val < ancestor.val) {
ancestor = ancestor.left;
} else if (p.val > ancestor.val && q.val > ancestor.val) {
ancestor = ancestor.right;
} else {
break;
}
}
return ancestor;
}
}