题2: 实现单例类 - done
题3: 二维数据查找 - done
题14: 调整数组顺序,使奇数在前偶数在后 - done
题20: 顺时针打印数组- done
题29: 数组中出现次数超过一半的数字 - done
题30: 最小的K个数 - done
题31: 连续子数组和的最大值 - done
题41: 有序数组查找“和为s”的两个数 - done
题51: 数组中重复的数字 - done
分析规律+快排定位的方法
题5: 单链表反向打印 - done
题13: 在O(1)时间删除单链表节点 - done
题15: 单链表查找倒数第k个节点 - done
题16: 单链表反转 - done
题17: 合并两个升序单链表 - done
题37: 两个单链表的第一个公共节点 - done
递归+多个指针
题7: 两个栈实现队列 - done
题7.1: 两个队列实现栈 - done
题9: 斐波那契数列 - done
题10: 二进制中1的个数 - done
题40: 数组中只出现一次的数 - done
题45: 约瑟夫问题 - done
题47: 不用加减乘除实现加法 - done
题47.1: 不使用新的变量,交换两个变量的值 - done
总结出数学公式+位运算+递归
题35: 第一个只出现一次的字符 - done
题58: 二叉树的下一个节点
题58: 判断一个二叉树是否对称
题60: 二叉树打印成多行
题61: 之字形打印二叉树
题62: 二叉搜索树的第k个节点
题62.1: 二叉树中序遍历
题2: 实现单例类
单例模式:
- 单例类确保该类只有唯一一个实例
- 单例类自己创建这一实例
- 单例类给所有其他对象提供这一实例
最简单的实现方式
// 饿汉式单例
public class Single {
private static Single single = new Single();
private Single() {
}
public static Single getSingle() {
return single;
}
}
解释
- static变量也叫静态变量,静态变量被所有的对象所共享,在内存中只有一个副本。饿汉式单例在类加载初始化时就创建好一个静态的对象供外部使用,除非系统重启,这个对象不会改变,所以本身就是线程安全的。
- 构造方法限定为private避免了类在外部被实例化
- Single类的唯一实例只能通过getSingle()方法访问
缺点
无论这个类是否被使用,都会被实例化,浪费内存
较好的实现方式
public class Single1 {
// volatile保证不同线程对这个变量进行操作时的可见性
// 即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的
private volatile static Single1 single1 = null;
private Single1() {
}
public static Single1 getSingle1() {
// 如果每次执行getSingle1都加锁,很影响性能
// 因此先判断,如果null,加锁初始化,如果非null,直接返回
if (single1 == null) {
synchronized (Single1.class) { // 锁,保证线程安全
if (single1 == null) {
single1 = new Single1();
}
}
}
return single1;
}
}
https://www.cnblogs.com/limingluzhu/p/5156659.html
题3: 二维数组查找
题14: 调整数组顺序,使奇数在前偶数在后
// 调整数组顺序,使奇数在前偶数在后
private static void handleArr(int[] arr) {
if (arr == null) {
return;
}
int start = 0;
int end = arr.length - 1;
int tem;
while (start != end) {
if ((arr[start] & 1) == 1) { // 奇数
start++;
}else { // 偶数
tem = arr[start];
arr[start] = arr[end];
arr[end] = tem;
end--;
}
}
}
题20: 顺时针打印数组
思路:确定圈数,针对圈中每条边分别打印,注意边界
// 顺时针打印数组
private static void circlePrintArr(int[][] arr) {
if (arr == null || arr.length <= 0 || arr[0].length <= 0) {
return;
}
int row = arr.length; // 总行数
int col = arr[0].length; // 总列数
int totalCir = (Math.min(row, col) + 1) >> 1; // 一共打印的圈数
for (int start = 0; start < totalCir; start++) {
printCircle(arr, row, col, start);
}
}
// row总行数,col总列数,start当前圈数
private static void printCircle(int[][] arr, int row, int col, int start) {
// 边界:左上角arr[start][start]
// 边界:右下角arr[endX][endY]
int endX = row - start - 1;
int endY = col - start - 1;
// 打印上边
for (int i = start; i <= endY; i++) {
System.out.print(arr[start][i] + " ");
}
// 打印右边
for (int i = start + 1; i <= endX; i++) {
System.out.print(arr[i][endY] + " ");
}
// 打印下边
if (endX > start) {
for (int i = endY - 1; i >= start; i--) {
System.out.print(arr[endX][i] + " ");
}
}
// 打印左边
if (endY > start) {
for (int i = endX - 1; i >= start + 1; i--) {
System.out.print(arr[i][start] + " ");
}
}
}
题29: 数组中出现次数超过一半的数字
描述:数组中又一个数字出现的次数,大于数组长度的一半,找出这个数
思路一:
- 快排定位方法
- 定位一次,如果定位不在中间,则重长的子数组在进行定位,直到定位在中间
思路二:
时间复杂度O(n) - 维护两个数,一个存值,一个存值出现的次数
- 遍历数组,相同-加次数,不同-减次数,次数为0-更新值
// 数组中出现次数超过一半的数字
private static Integer moreThanHalfNum(int[] arr) {
if (arr == null || arr.length <= 1) {
return null;
}
int key = arr[0];
int cnt = 1;
for (int i = 1; i < arr.length; i++) {
if (cnt == 0) {
key = arr[i];
cnt++;
}else if (arr[i] == key) {
cnt++;
}else {
cnt--;
}
}
return key;
}
题30: 最小的K个数
思路:利用快排的定位方法
- 定位,判断左边个数如果不为k-1,继续定位,直到左边个数为k-1
// 最小的k个数
private static void leastKNumbers(int[] arr, int k) {
if (arr == null || k <= 0 || arr.length < k) {
return;
}
int start = 0;
int end = arr.length - 1;
int pos = Sort.position(arr, start, end);
while (pos != k - 1) {
if (pos > k - 1) {
end = pos - 1;
}else {
start = pos + 1;
}
pos = Sort.position(arr, start, end);
}
for (int i = 0; i < k; i++) {
System.out.print(arr[i] + " ");
}
}
题31: 连续子数组和的最大值
思路:时间复杂度O(n)
- 用f(i)来表示,所有以i结尾的子数组中,子数组和的最大值
- 那么题目答案转换为,求max(f(i))
- 如果f(i-1)小于0,那么f(i) = arr[i],如果f(i-1)大于0,那么f(i) = f(i-1) + arr[i]
// 连续子数组,和的最大值
private static Integer findMax(int[] arr) {
if (arr == null || arr.length == 0) {
return null;
}
int sum = 0; // f(i)
int arrMax = 0x80000000; // max(f(i))
for (int i = 0; i <= arr.length - 1; i++) {
if (sum <= 0) {
sum = arr[i];
}else {
sum = sum + arr[i];
}
if (sum > arrMax) {
arrMax = sum;
}
}
return arrMax;
}
题41: 有序数组查找“和为s”的两个数
思路:头尾两个指针
// 有序数组查找“和为s”的两个数
private static boolean findNumWithSum(int[] arr, int sum) {
if (arr == null || arr.length < 2) {
return false;
}
int start = 0;
int end = arr.length - 1;
int curSum;
while (end > start) {
curSum = arr[start] + arr[end];
if (curSum == sum) {
System.out.println(arr[start]);
System.out.println(arr[end]);
return true;
}else if (curSum > sum) {
end--;
}else {
start++;
}
}
return false;
}
题51: 数组中重复的数字
描述:长度为n的数组,没个数都在[0, n-1]内,求数组中是否有重复数字
思路:分析数组规律,如果数组中没有重复数字,那么下标为i的位置,值也为i
// 数组中重复的数字
private static boolean isRepeat(int[] arr) {
if (arr == null || arr.length == 0) {
return false;
}
int tem;
for (int i = 0; i < arr.length; i++) {
while (arr[i] != i) {
if (arr[i] == arr[arr[i]]) {
System.out.println(arr[i]);
return true;
}
tem = arr[i];
arr[i] = arr[tem];
arr[tem] = tem;
}
}
return false;
}
题5: 单链表反向打印
提示:递归
private static void reverseList(Node node) {
if (node == null) {
return;
}
if (node.next == null) {
System.out.println(node.num);
return;
}
reverseList(node.next);
System.out.println(node.num);
}
题13: 在O(1)时间删除单链表节点
描述:假定待删除节点在给定链表内
思路:有O(1)的时间要求,不能遍历,可以用“待删除节点的next节点”的内容,覆盖“待删除节点”的内容。
比如
- 给定链表:n1 -> n2 -> n3 -> n4 -> null
-
删除节点:n2
此时一定不能用n2 = n3,因为n2 = n3达到的效果如下图,n2原来指向b93,执行n2 = n3后,n2指向80c,而原链表并没有变化
而应该用n3的内容覆盖n2的内容,然后删除n3
// O(1)时间内删除单链表节点
private static void delNode(Node head, Node delNode) {
if (head == null || delNode == null) {
return;
}
Node tem;
// 待删除节点不是尾节点
if (delNode.next != null) {
tem = delNode.next;
delNode.num = tem.num;
delNode.next = tem.next;
}else if (head != delNode){ // 待删除节点不是头节点,是尾节点
tem = head;
while (tem.next != delNode) {
tem = tem.next;
}
tem.next = null;
delNode = null;
}else { // 链表长度1,删除头节点
head = null;
delNode = null;
}
tem = null;
}
虽然有一种场景需要遍历n-1,但是平均时间复杂度仍然是O(1),不再解释
题15: 单链表查找倒数第k个节点
思路:先做条件判断,以免程序崩溃
- 链表为空
- k <= 0
- k大于链表长度
// 单链表倒数第k个节点
private static Node findKTail(Node head, int k) {
if (head == null || k <= 0) {
return null;
}
Node fast = head;
// 快慢指针,快指针先走k-1步
for (int i = 0; i < k - 1; i++) {
fast = fast.next;
if (fast == null) { // k大于链表长度
return null;
}
}
Node slow = head;
while (fast.next != null) {
slow = slow.next;
fast = fast.next;
}
return slow;
}
题16: 单链表反转
思路:三个指针
private Node singleListRoll(Node node) {
if (node == null) {
return null;
}
Node pre = null;
Node cur = node;
Node nex;
while(cur != null) {
nex = cur.next;
cur.next = pre;
pre = cur;
cur = nex;
}
return pre;
}
题17: 合并两个升序单链表
思路:用递归做
// 合并两个升序链表
private static Node merge1(Node h1, Node h2) {
if (h1 == null) {
return h2;
}
if (h2 == null) {
return h1;
}
Node h = null;
if (h1.num <= h2.num) {
h = h1;
h.next = merge1(h1.next, h2);
}else {
h = h2;
h.next = merge1(h1, h2.next);
}
return h;
}
题37: 两个单链表的第一个公共节点
思路:时间复杂度O(n)
- 先得到两个链表长度
- 长链表先走
- 两个链表一起走,第一个相同
// 两个单链表第一个相同节点
private static Node findFirstNode(Node h1, Node h2) {
if (h1 == null || h2 == null) {
return null;
}
int l1 = nodeLength(h1);
int l2 = nodeLength(h2);
Node longNode = h1;
Node shorNode = h2;
int dif = l1 - l2;
if (l2 > l1) {
longNode = h2;
shorNode = h1;
dif = l2 - l1;
}
for (int i = 0; i < dif; i++) {
longNode = longNode.next;
}
while (longNode != null && shorNode != null) {
if (longNode == shorNode) {
break;
}
longNode = longNode.next;
shorNode = shorNode.next;
}
return longNode;
}
private static int nodeLength(Node node) {
int l = 0;
Node n = node;
while (n != null) {
l++;
n = n.next;
}
return l;
}
题7: 两个栈实现队列
思路:
- 两个栈,一个做“入栈”,一个做“出栈”
- “出栈”为空,“入栈”数据导入到“出栈”,继续
public class WQueue {
private Stack stack1 = new Stack();
private Stack stack2 = new Stack();
// 在队列尾部插入元素
public void appendTail(int num) {
stack1.push(num);
}
// 从队列顶部删除一个元素
public Integer deleteHead() throws Exception {
if (stack2.empty()) {
while (!stack1.empty()) {
stack2.push(stack1.pop()); // push入栈,pop出栈
}
}
if (stack2.empty()) {
throw new Exception("something wrong");
}
return stack2.pop();
}
}
题7.1: 两个队列实现栈
思路:
- 任何时候都维持“一个队列空,另一个队列非空”
- 出栈时,把“非空队列”除队尾外的数据,导入“空队列”,删除“非空队列”对尾元素
// 两个队列实现栈
public class WStack {
private Queue queue1 = new LinkedList();
private Queue queue2 = new LinkedList();
// 入栈
public void append(int num) {
if (queue1.isEmpty()) {
queue2.offer(num); // offer入队列,poll出队列
}else {
queue1.offer(num);
}
}
// 出栈
public Integer delete() {
Integer res = null;
if (!queue1.isEmpty()) {
while (queue1.size() > 1) {
queue2.offer(queue1.poll());
}
res = queue1.poll();
}else if (!queue2.isEmpty()) {
while (queue2.size() > 1) {
queue1.offer(queue2.poll());
}
res = queue2.poll();
}
return res;
}
}
题9: 斐波那契数列
f(0) = 0
f(1) = 1
f(n) = f(n-1) + f(n-2)
解法一:递归
// 斐波那契数列
private static int fibo(int n) {
if (n < 0) {
return 0;
}
if (n <= 1) {
return n;
}
return fibo(n - 1) + fibo(n - 2);
}
上述解法存在很多重复计算,比如计算f(7),需要计算3次f(4),随着n的增大,重复计算节点会指数递增,而直接计算反而好些
解法二:直接计算
// 斐波那契数列
private static int fibo1(int n) {
if (n < 0) {
return 0;
}
if (n <= 1) {
return n;
}
int n1 = 0;
int n2 = 1;
int sum = n1 + n2;
for (int i = 2; i <= n; i++) {
sum = n1 + n2;
n1 = n2;
n2 = sum;
}
return sum;
}
很多问题可以转化为斐波那契数列,遇到问题先分析规律,先走一步找规律
题10: 二进制中1的个数
输入int数,输出二进制中1的个数
思路:
- 不能用除以2,因为负数的时候会出错,比如-1的二进制是32个1,应该输出32
- 每一位都做&运算
// 二进制中1的个数
private static int numOf1(int n) {
int cnt = 0;
int flag = 1;
while (flag != 0) {
if ((n & flag) == flag) {
cnt++;
}
flag = flag << 1;
}
return cnt;
}
tips:把一个整数减去1,在和原来的整数做&运算,得到的结果相当于把整数的二进制表示中最右边的一个1变为0,很多问题可以用这个思路解决
题40: 数组中只出现一次的数
描述:给定数组,只有两个数只出现一次,其余数均出现了两次,找出这两个数
思路:
- 任何一个数字异或它自己都等于0,异或满足结合律
- 如果数组中“只有一个数字只出现一次”,那么把整个数组异或一边,就能得到要找的数字
- 如果数组中“只有一个数字只出现一次”,尝试把数字分为两部分,每部分“只有一个数字出现一次”
- 这两个数字是不同的,因此他们异或的结果,二进制一定有一位是1,把这一位作为标识位,把数组分为两部分,问题解决
// 数组中只出现一次的数字
private static void findTwoInt(int[] arr) {
if (arr == null || arr.length < 2) {
return;
}
int sum = 0;
for (int i = 0; i < arr.length; i++) {
sum ^= arr[i];
}
// 找出key1 ^ key2 中为1的位(倒数第index位)
int index = 1;
while ((sum & 1) != 1) {
sum = sum >> 1;
index++;
}
int key1 = 0;
int key2 = 0;
for (int i = 0; i < arr.length; i++) {
if (splitArr(arr[i], index)) {
key1 ^= arr[i];
}else {
key2 = key2 ^ arr[i];
}
}
System.out.println(key1 + " " + key2);
}
private static boolean splitArr(int num, int index) {
num = num >> (index - 1);
return (num & 1) == 1;
}
题45: 约瑟夫问题
描述:0,1...n-1这n个人按顺序排成一个圆圈,从0开始报数,报到m-1的人退出,剩下的人继续从0开始报数,求最后胜利者的编号
比如:0,1,2,3,4这五个数,m=3,则依次删除2,0,4,1,最后3胜出
思路:总结出数学规律
- 用f(n)表示最后胜出的人
- 那么第一个一定是(m-1)%n退出,然后从k=m%n开始,继续报数
k, k+1, k+2 ... n-2, n-1, 0, 1, 2 ... k-2 -
剩下的问题就完全变为了n-1个人的新约瑟夫环问题,我们把他们的编号做一下转换
- 原问题的解f(n),等于左边环的解(也就是第2步的环的解,这个好理解)
- 另外不管编号如何变化,最后胜出者的位置是不变的(也就是第3步,左右两个环的解位置相同)
- 左边编号x',到右边编号x,有映射关系x'=(x+k)%n
- 假设最后的解位于图标出的位置,即
-- f(n)=x'
-- f(n-1)=x
-- x'=(x+k)%n - 于是,f(n)=[f(n-1)+k]%n=[f(n-1)+m]%n (n>1的时候)
// 约瑟夫环
private static int lastWin(int n, int m) {
if (n <= 0 || m <= 0) {
return -1;
}
int last = 0;
for (int i = 2; i <= n; i++) {
last = (last + m) % i;
}
return last;
}
题47: 不用加减乘除实现加法
思路:
- 二进制,加法用异或代替,进位用与代替
- 如果进位到了符号位,比如0x40000000+0x40000000,那么将得到0x80000000,也就是1073741824+1073741824=-2147483648,这是不合理的,但是如果直接用十进制加法得到的也是这个结果,因为这种情况溢出了,所以我们的算法还是正确的
// 不用加减乘除做加法
private static int sum(int i1, int i2) {
int sum; // 不考虑进位的和
int carry; // 进位
do {
sum = i1 ^ i2;
carry = (i1 & i2) << 1;
i1 = sum;
i2 = carry;
}while (carry != 0); // 直到没有进位
return sum;
}
题47.1: 不使用新的变量,交换两个变量的值
描述:交换a、b
思路:
- a=a+b; b=a-b; a=a-b;
- a=a^b; b=a^b; a=a^b; // 异或的结合率
题35: 第一个只出现一次的字符
思路:空间换时间
// 第一个只出现一次的字符
private static Character firstSingle(String str) {
if (str == null || str.length() == 0) {
return null;
}
int[] arr = new int[256]; // 下标-字符的asscii码,值-字符出现次数
for (int i = 0; i < str.length(); i++) {
arr[str.charAt(i)] ++;
}
for (int i = 0; i < str.length(); i++) {
if (arr[str.charAt(i)] == 1) {
return str.charAt(i);
}
}
return null;
}
题58: 二叉树的下一个节点
描述:给定一个二叉树的其中一个节点,树中的节点除了有指向左右子节点的指针,还有一个指向父节点的指针,找出中序遍历的下一个节点
题58: 判断一个二叉树是否对称
思路:定义一种遍历方式,根-右-左。判断先序遍历和此遍历得到的序列是否一致
private static boolean isSymm(TreeNode root) {
return isSymm(root, root);
}
private static boolean isSymm(TreeNode root1, TreeNode root2) {
if (root1 == null && root2 == null) {
return true;
}
if (root1 == null || root2 == null) {
return false;
}
if (root1.data != root2.data) {
return false;
}
return isSymm(root1.left, root2.right) && isSymm(root1.right, root2.left);
}
题60: 二叉树打印成多行
描述:给定一个二叉树,每一层打印一行,每一层打印顺序从左到右
思路:用一个队列存放节点,用变量控制是否换行
// 二叉树打印成多行
private static void printTree(TreeNode root) {
if (root == null) {
return;
}
Queue queue = new LinkedList();
queue.offer(root);
int curUnHandle = 1; // 当前层未打印个数
int nextTotal = 0; // 下一层总个数
TreeNode node;
while (!queue.isEmpty()) {
node = queue.poll();
System.out.print(node.data + " ");
curUnHandle--;
if (node.left != null) {
queue.offer(node.left);
nextTotal++;
}
if (node.right != null) {
queue.offer(node.right);
nextTotal++;
}
if (curUnHandle == 0) {
System.out.println();
curUnHandle = nextTotal;
nextTotal = 0;
}
}
}
题61: 之字形打印二叉树
描述:给定一个二叉树,按之字形打印二叉树,即第一行从左到右,第二行从右到左,依次类推
思路:分析规律,两个栈
// 二叉树之字形打印
private static void printTree2(TreeNode root) {
if (root == null) {
return;
}
Stack stack1 = new Stack(); // 先左后右
Stack stack2 = new Stack(); // 先右后左
stack1.push(root);
TreeNode node;
while (!stack1.empty() || !stack2.empty()) {
if (!stack1.empty()) {
while(!stack1.empty()) {
node = stack1.pop();
System.out.print(node.data + " ");
if (node.left != null) {
stack2.push(node.left);
}
if (node.right != null) {
stack2.push(node.right);
}
}
System.out.println();
}else {
while (!stack2.empty()) {
node = stack2.pop();
System.out.print(node.data + " ");
if (node.right != null) {
stack1.push(node.right);
}
if (node.left != null) {
stack1.push(node.left);
}
}
System.out.println();
}
}
}
题62: 二叉搜索树的第k个节点
描述:给定二叉排序树,找出其中第k大的节点。
思路:二叉排序树的中序遍历是从小到大的数列
题62.1: 二叉树中序遍历
// 二叉树中序遍历-递归
private static void midPrintTree1(TreeNode node) {
if (node == null) {
return;
}
midPrintTree1(node.left);
System.out.print(node.data + " ");
midPrintTree1(node.right);
}
// 二叉树中序遍历-非递归
private static void midPrintTree2(TreeNode node) {
if (node == null) {
return;
}
Stack stack = new Stack();
while(node != null || !stack.empty()) {
while(node != null) {
// 前序遍历在这里打印即可
stack.push(node);
node = node.left;
}
if(!stack.empty()) {
node = stack.pop();
System.out.print(node.data + " ");
node = node.right;
}
}
}