24点游戏
递归 穷举法 暴力破解
从数组中选出两个数字,对其选择加减乘除四种运算之一,然后将得出的值代替选出的两个数字,剩下三个数字同理。最后只会剩下一个数字,直接判断这个数字与24是否相等,由于是实数除法,所以要考虑精度。然后减法和除法有两种情况也要考虑,除数不能为0也要考虑
/**
* @param {number[]} nums
* @return {boolean}
*/
var judgePoint24 = function(nums) {
var len=nums.length;
if(len==1){
return Math.abs(nums[0]-24)<0.00001;
}else{
for(var i=0;i<nums.length;i++){
for(var j=i+1;j<nums.length;j++){
var newarr=nums.slice();//复制一份原数组
newarr.splice(j,1);//先删除索引高的值 这样不会影响前面值的位置
newarr.splice(i,1);//再删除索引低的值
var n1=nums[i];
var n2=nums[j];
var isValid=false;//初始化结果(是否能组成24)
//加
isValid=isValid||judgePoint24(newarr.concat(n1+n2));//将新值添加到数组中
//减
isValid=isValid||judgePoint24(newarr.concat(n1-n2));
isValid=isValid||judgePoint24(newarr.concat(n2-n1));
//乘
isValid=isValid||judgePoint24(newarr.concat(n1*n2));
//除
if(!Math.abs(n1-0)<0.000001){
isValid=isValid||judgePoint24(newarr.concat(n2/n1));
}
if(!Math.abs(n2-0)<0.000001){
isValid=isValid||judgePoint24(newarr.concat(n1/n2));
}
//如果存在等于24的情况 直接返回 退出程序 如果没有 则继续循环
if(isValid){
return true;
};
}
}
return false;
}
};
数字范围按位与
给定范围 [m, n],其中 0 <= m <= n <= 2147483647,返回此范围内所有数字的按位与(包含 m, n 两端点)。
采用位移
问题的本质为,给定两个整数,求对应的二进制字符串的公共前缀
将两个数字不断的向右移动,直到数字相等,再在其右边补上0
/**
* @param {number} m
* @param {number} n
* @return {number}
*/
var rangeBitwiseAnd = function(m, n) {
let shift = 0;
// 找到公共前缀
while (m != n) {
//右移
m = m >> 1;
n = n >> 1;
shift++;
}
//左移补零
return m << shift;
};
位运算符
位运算符会把数字转换成二进制进行运算,再返回十进制数
剑指offer-用两个栈实现队列
栈1实现插入操作,栈2实现删除操作
栈2若不为空,则直接删除顶部元素;若栈2为空且栈1为空,则代表没有值,返回-1;若栈2为空栈1不为空,则一个一个的将栈1中的元素添加到栈2中,直到栈1为空;
另外一种方法是栈1实现删除操作,栈2辅助栈1实现插入
注意这里判断栈是否为空不要用==null!!!!要用isEmpty()!!!不然会报错的!!!
import java.util.Stack;
public class Solution {
Stack<Integer> stack1 = new Stack<Integer>();
Stack<Integer> stack2 = new Stack<Integer>();
public void push(int node) {
if(stack1==null){
stack1.push(node);
}else{
while(!stack1.isEmpty()){
stack2.push(stack1.pop());
}
stack1.push(node);
while(!stack2.isEmpty()){
stack1.push(stack2.pop());
}
}
}
public int pop() {
if(stack1.isEmpty()){
return -1;
}else{
return stack1.pop();
}
}
}
import java.util.Stack;
public class Solution {
Stack<Integer> stack1 = new Stack<Integer>();
Stack<Integer> stack2 = new Stack<Integer>();
public void push(int node) {
stack1.push(node);
}
public int pop() {
if(!stack2.isEmpty()) return stack2.pop();
if(stack1.
()) return -1;
while(!stack1.isEmpty())
stack2.push(stack1.pop());
return stack2.pop();
}
}
二叉树的镜像-递归
/**
public class TreeNode {
int val = 0;
TreeNode left = null;
TreeNode right = null;
public TreeNode(int val) {
this.val = val;
}
}
*/
public class Solution {
public TreeNode Mirror(TreeNode root) {
if(root==null){
return null;
}else{
//将左右子树交换
TreeNode left=Mirror(root.right);
TreeNode right=Mirror(root.left);
root.left=left;
root.right=right;
return root;
}
}
}
回溯算法
组合
给定两个整数 n 和 k,返回 1 … n 中所有可能的 k 个数的组合。
示例:
输入: n = 4, k = 2
输出:
[
[2,4],
[3,4],
[2,3],
[1,2],
[1,3],
[1,4],
]
/**
* @param {number} n
* @param {number} k
* @return {number[][]}
*/
var combine = function(n, k) {
let res=[];
if(k<=0||n<k){
return res;
}else{
//保存每一种组合的结果
let path=[];
dfs(n,k,1,path,res);
return res;
}
};
function dfs(n,k,begin,path,res){
//递归的终止条件
//path的长度等于k
if(path.length==k){
//因为res是一个二维数组,所以这里要以数组的形式插入
//每当找到一组k个数的组合,就插入到res中
res.push([...path]);
return;
}
//题目的设定:从1开始 直到n
for(let i=begin;i<=n;i++){
path.push(i);
dfs(n,k,i+1,path,res);
//重点是这里
//例如遍历到2,3,4 则要回退一位数,遍历下一位数2,3,5
path.pop();
}
}
优化:
分析搜索起点的上届进行剪枝
这种方法不容易想到,其中的规律不容易发现在这里插入代码片
比如对于数组[1,2,3,4,5,6],要组合3位数,则起点到4的位置就不用再继续往下遍历了
例如:n = 6 ,k = 4。
path.size() == 1 的时候,接下来要选择 33 个数,搜索起点最大是 44,最后一个被选的组合是 [4, 5, 6];
path.size() == 2 的时候,接下来要选择 22 个数,搜索起点最大是 55,最后一个被选的组合是 [5, 6];
path.size() == 3 的时候,接下来要选择 11 个数,搜索起点最大是 66,最后一个被选的组合是 [6];
再如:n = 15 ,k = 4。
path.size() == 1 的时候,接下来要选择 33 个数,搜索起点最大是 1313,最后一个被选的是 [13, 14, 15];
path.size() == 2 的时候,接下来要选择 22 个数,搜索起点最大是 1414,最后一个被选的是 [14, 15];
path.size() == 3 的时候,接下来要选择 11 个数,搜索起点最大是 1515,最后一个被选的是 [15];
可以归纳出:
搜索起点的上界 + 接下来要选择的元素个数 - 1 = n
其中,接下来要选择的元素个数 = k - path.size(),整理得到:
搜索起点的上界 = n - (k - path.size()) + 1
所以,我们的剪枝过程就是:把 i <= n 改成 i <= n - (k - path.length) + 1
另一种递归方法
关于递归的终止条件:
如果不剪枝的话,递归结束条件为begin==n+1
/**
* @param {number} n
* @param {number} k
* @return {number[][]}
*/
var combine = function(n, k) {
let res = [];
if (k <= 0 || n < k) {
return res;
} else {
//保存每一种组合的结果
let path = [];
dfs(n, k, 1, path, res);
return res;
}
};
function dfs(n, k, begin, path, res) {
if (k == 0) {
res.push([...path]);
return;
}
//if(begin==n+1)
if (begin > n - k + 1) {
return;
}
// 不选当前考虑的数 begin,直接递归到下一层
dfs(n, k, begin + 1, path, res);
// 选当前考虑的数 begin,递归到下一层的时候 k - 1,这里 k 表示还需要选多少个数
path.push(begin);
dfs(n, k - 1, begin + 1, path, res);
path.pop();
}
全排列
给定一个 没有重复 数字的序列,返回其所有可能的全排列。
示例:
输入: [1,2,3]
输出:
[
[1,2,3],
[1,3,2],
[2,1,3],
[2,3,1],
[3,1,2],
[3,2,1]
]
说明:
每一个结点表示了求解全排列问题的不同的阶段,这些阶段通过变量的「不同的值」体现,这些变量的不同的值,称之为「状态」;
使用深度优先遍历有「回头」的过程,在「回头」以后, 状态变量需要设置成为和先前一样 ,因此在回到上一层结点的过程中,需要撤销上一次的选择,这个操作称之为「状态重置」;
深度优先遍历,借助系统栈空间,保存所需要的状态变量,在编码中只需要注意遍历到相应的结点的时候,状态变量的值是正确的,具体的做法是:往下走一层的时候,path 变量在尾部追加,而往回走的时候,需要撤销上一次的选择,也是在尾部操作,因此 path 变量是一个栈;
深度优先遍历通过「回溯」操作,实现了全局使用一份状态变量的效果。
使用编程的方法得到全排列,就是在这样的一个树形结构中完成 遍历,从树的根结点到叶子结点形成的路径就是其中一个全排列。
设计状态变量
- 首先这棵树除了根结点和叶子结点以外,每一个结点做的事情其实是一样的,即:在已经选择了一些数的前提下,在剩下的还没有选择的数中,依次选择一个数,这显然是一个 递归 结构;
- 递归的终止条件是: 一个排列中的数字已经选够了 ,因此我们需要一个变量来表示当前程序递归到第几层,我们把这个变量叫做 depth,或者命名为 index ,表示当前要确定的是某个全排列中下标为 index 的那个数是多少;
- 布尔数组 used,初始化的时候都为 false 表示这些数还没有被选择,当我们选定一个数的时候,就将这个数组的相应位置设置为 true ,这样在考虑下一个位置的时候,就能够以 O(1)O(1) 的时间复杂度判断这个数是否被选择过,这是一种「以空间换时间」的思想。
这些变量称为「状态变量」,它们表示了在求解一个问题的时候所处的阶段。需要根据问题的场景设计合适的状态变量。
/**
* @param {number[]} nums
* @return {number[][]}
*/
var permute = function(nums) {
//存储最终的结果
let res=[];
if(nums.length==0){
return res;
}else{
//初始化为false 表示这些数都没有被选择
let used=[];
//存储每一次全排列的值
//path相当于一个栈
let path=[];
dfs(nums,path,res,used,0);
return res;
}
};
//这里的depth没有用,代码里并没有对他进行判断,删掉也可以
function dfs(nums,path,res,used,depth){
//如果全都选择了一遍,则把值添加到res中
if(path.length==nums.length){
res.push([...path]);
return;
}else{
for(let i=0;i<nums.length;i++){
//如果nums[i]没有被选择
if(used[i]!=true){
path.push(nums[i]);
//表示已经被选择
used[i]=true;
dfs(nums,path,res,used,depth+1);
// 注意:下面这两行代码发生 「回溯」,回溯发生在从 深层结点 回到 浅层结点 的过程,代码在形式上和递归之前是对称的
used[i]=false;
path.pop();
}
}
}
}
二叉搜索树与双向链表
输入一棵二叉搜索树,将该二叉搜索树转换成一个排序的循环双向链表。要求不能创建任何新的节点,只能调整树中节点指针的指向。
第一种 将二叉搜索树中序遍历保存到一个数组中,然后对数组中的节点一个一个进行连接
例如:(自己写的js代码感觉没有问题但是运行有错误,思路是一样的)
class Solution {
public Node treeToDoublyList(Node root) {
if (root == null) {
return root;
}
Queue<Node> queue=new LinkedBlockingDeque<>();
// 中序遍历,得到递增队列
queue=dfs(root,queue);
// 先将第一个结点pop出来,方便操作
Node head=queue.remove();
Node pre=head;
Node cur=head;
while (!queue.isEmpty()){
cur=queue.remove();
// 上一个结点的右节点指到当前结点
pre.right=cur;
// 当前结点的左结点指到上一个结点
cur.left=pre;
pre=cur;
}
// 最后将第一个结点与最后一个结点相连
head.left=pre;
pre.right=head;
return head;
}
public Queue dfs(Node root,Queue queue){
if (root == null) {
return null;
}
dfs(root.left,queue);
queue.add(root);
dfs(root.right,queue);
return queue;
}
}
第二种
一边中序遍历一边改变节点的指向,最后再让头节点和尾节点互相指向
/**
* // Definition for a Node.
* function Node(val,left,right) {
* this.val = val;
* this.left = left;
* this.right = right;
* };
*/
/**
* @param {Node} root
* @return {Node}
*/
let head, pre
var treeToDoublyList = function(root) {
if(root == null) {
return null
}
head = null, pre = null
dfs(root)
//将头节点和尾节点互相指向
head.left = pre
pre.right = head
return head
};
var dfs = function(cur){
if(cur == null) {return}
dfs(cur.left)
//如果pre为空,即当前节点cur前面没有节点,表示到头了
//所以cur就是头节点
if(pre == null) {
head = cur
}else{
pre.right = cur
cur.left = pre
}
//pre跟随cur移动
//pre就是上一个cur
pre = cur
dfs(cur.right)
}
求出两个字符串的最长公共字符串
问题:有两个字符串str1和str2,求出两个字符串中最长公共字符串。
例如:“acbbsdef”和"abbsced"的最长公共字符串是“bbs”
为了避免后续查找对角线长度的操作:
可以先计算二维矩阵值的时候顺便计算出来当前最长的公共子串的长度;
即:某个二维矩阵元素的值由item[i][j]=1演变为item[i][j]=1 +item[i-1][j-1]。
public class MaxStringDemo {
public static void main(String[] args) {
String aa = "abc123edf";
String bb = "bc123jg";
maxUtil2(aa, bb);
System.out.println(maxUtil2(aa, bb));
}
public static StringBuilder maxUtil2(String str1, String str2) {
//把字符串转成字符数组
char[] arr1 = str1.toCharArray();
char[] arr2 = str2.toCharArray();
// 把两个字符串分别以行和列组成一个二维矩阵
int[][] temp = new int[arr1.length][arr2.length];
// 存储最长公共子串长度
int length = 0;
//start表明最长公共子串的起始点,end表明最长公共子串的终止点
int end = 0;
int start = 0;
初始化二维矩阵中的第一行
for (int i = 0; i < arr2.length; i++) {
temp[0][i] = (arr1[0] == arr2[i]) ? 1 : 0;
}
//初始化二维矩阵中的第一列
for (int j = 0; j < arr1.length; j++) {
temp[j][0] = (arr2[0] == arr1[j]) ? 1 : 0;
}
//嵌套for循环:比较二维矩阵中每个点对应行列字符中否相等,相等的话值设置为1,否则设置为0
for (int i = 1; i < arr1.length; i++) {
for (int j = 1; j < arr2.length; j++) {
if (arr1[i] == arr2[j]) {
temp[i][j] = temp[i - 1][j - 1] + 1;
if (temp[i][j] > length) {
length = temp[i][j];
//这里取的是j 因为j是内层循环
end = j;
}
} else
temp[i][j] = 0;
}
}
//求出最长公共子串的起始点
start=end-length+1;
StringBuilder sb=new StringBuilder();
//通过查找出值为1的最长对角线就能找到最长公共子串
for (int j = start; j < end+1; j++) {
sb.append(arr2[j]);
}
return sb;
}
}