1、认识复杂度、对数器、二分法与异或运算
(1)复杂度
常见常数时间操作:
- 常见的算术运算(+、-、*、/、%等)
- 常见的位运算(>>>、>>、<<、|、&、^等)
- 赋值、比较、自增、自减操作等
- 数组寻址操作
选择排序
public class Code01_SelectionSort {
public static void selectionSort(int[] arr){
if (arr == null || arr.length < 2) {
return;
}
// 第一个for是控制那个范围做什么事
for (int i = 0; i < arr.length - 1; i++) {
int minIndex = i;//找到最小值在那个位置i ~ (n-1),找到就放i位置
for (int j = i + 1; j < arr.length; j++) {
minIndex = arr[j] < arr[minIndex] ? j : minIndex;
}
swap(arr,i,minIndex);
}
}
private static void swap(int[] arr,int i,int j) {
int tmp = arr[i];
arr[i] = arr[j];
arr[j] = tmp;
}
}
冒泡排序
public class Code02_BubbleSort {
public static void bubbleSort(int[] arr) {
if (arr == null || arr.length < 2) {
return;
}
for (int i = arr.length - 1; i > 0; i--) {
for (int j = 0; j < i; i++) {
if (arr[j] < arr[j + 1]) {
swap(arr,i,j);
}
}
}
}
private static void swap(int[] arr,int i,int j){
arr[i] = arr[i] ^ arr[j];
arr[j] = arr[i] ^ arr[j];
arr[i] = arr[i] ^ arr[j];
}
}
插入排序
public class Code01_InsertionSort {
public static void insertSort(int[] arr){
if (arr == null || arr.length < 2) {
return;
}
for (int i = 1; i < arr.length;i++) {
for (int j = i - 1; j >= 0 && arr[j] > arr[j + 1]; j--) {
swap(arr,j,j+1);
}
}
}
static void swap(int[] arr,int i,int j) {
arr[i] = arr[i] ^ arr[j];
arr[j] = arr[i] ^ arr[j];
arr[i] = arr[i] ^ arr[j];
}
}
一个问题的最优解是什么?
一般情况下,认为解决一个问题的算法流程,在时间复杂度的指标上,一定尽可能地低,先满足了时间复杂度最低这个指标之后,在使用最少空间的算法流程,这样就叫问题的最优解。
(2)认识对数器
- 你想要测的方法a
- 实现复杂度不好但容易实现的方法b
- 实现 一个随机样本产生器
- 把方法a和方法b跑相同的随机样本,看看得到的结果是否一样
- 如果有一个随机样本使得比对结果不一致,打印样本进行人工干涉,对方法进行
- 当样本数量很多时比对测试依然正确,可以确定方法a已经正确
(3)二分法
有序二分查找
public class Code01_Binary {
public static boolean binary(int[] arr,int num) {
if (arr == null || arr.length == 0) {
return false;
}
int l = 0;
int r = arr.length - 1;
while (l < r) {
int mid = l + ((r - l) >> 2);
if (arr[mid] == num) {
return true;
}
else if (arr[mid] > num) {
r = mid - 1;
}
else {
l = mid + 1;
}
}
return arr[l] == num;
}
}
无序的二分
// 局部二分
public static int getLessIndex(int[] arr) {
if (arr.length == 0 || arr == null) {
return -1;
}
if (arr.length == 1 || arr[0] < arr[1]) {
return 0;
}
if (arr[arr.length - 1] < arr[arr.length - 2]) {
return arr.length - 1;
}
int left = 1,right = arr.length - 2,mid = 0;
while (left < right) {
mid = (left+right) / 2;
if (arr[mid] > arr[mid - 1]) {
right = mid - 1;
}
else if (arr[mid] > arr[mid + 1]) {
left = mid + 1;
}
else {
return mid;
}
}
return left;
}
(4)认识异或运算
记住一句话:异或运算就记成无进位相加
异或运算的作用:
- 参与运算的两个值,如果两个相应bit位相同,则结果为0,否则为1。
- 特点:
- 00=0,01=1 0异或任何数=任何数
- 10=1,11=0 1异或任何数-任何数取反
- 任何数异或自己=把自己置0
题目1:如果不用额外变量交换两个数
题目2:一个数组中有一种数出现了奇数次,其他数都出现偶数次,怎么找到并打印这种数。
2 链表结构、栈、队列、递归行为、哈希表和有序表
(1)单向链表和双向链表
单链表和双链表如何反转
public class Code01_ReverseList {
public static class Node{
public int value;
public Node next;
public Node(int data) {
value = data;
}
}
public static class DoubleNode{
public int value;
public DoubleNode next;
public DoubleNode last;
public DoubleNode(int data) {
value = data;
}
}
// 反转双链表
public static DoubleNode reverseLinkedList(DoubleNode head) {
DoubleNode pre = null;
DoubleNode next = null;
while (head != null) {
next = head.next;
head.next = pre;
head.last = next;
pre = head;
head = next;
}
return head;
}
// 反转单链表
public static Node reverseDoubleList(Node head) {
Node pre = null;
Node next = null;
while (head != null) {
next = head.next;
head.next = pre;
pre = head;
head = next;
}
return head;
}
}
把给定值都删除
public class Code01_DeleteGivenValue {
public static class Node{
public int value;
public Node next;
public Node(int data) {
value = data;
}
}
public static Node removeValue(Node head,int num) {
while (head != null) {
if (head.value != num) {
break;
}
head = head.next;
}
Node pre = head;
Node cur = head;
while (cur != null) {
if (cur.value == num) {
pre.next = cur.next;
}
else {
pre = cur;
}
cur = cur.next;
}
return head;
}
}
(2)栈和队列
逻辑概念:
- 栈:数据先进后出
- 队列:数据先进先出
栈和队列的实际实现:
- 双向链表实现
- 数组实现
题目1:pop、push、getMin操作的时间复杂度都是O(1),设计的栈类型可以使用现有的栈结构
public class Code1_GetMinStack {
public static class MyStack1 {
private Stack<Integer> stackData;
private Stack<Integer> stackMin;
public MyStack1(){
this.stackData = new Stack<Integer>();
this.stackMin = new Stack<Integer>();
}
public void push(int newNum) {
if (this.stackMin.isEmpty()) {
this.stackMin.push(newNum);
}
else if (newNum <= this.getmin()) {
this.stackData.push(newNum);
}
}
public int pop(){
if (this.stackData.isEmpty()) {
throw new RuntimeException("You stack is empty");
}
int value = this.stackData.pop();
if (value == this.getmin()) {
this.stackMin.pop();
}
return value;
}
public int getmin() {
if (this.stackMin.isEmpty()) {
throw new RuntimeException("You stack is empty");
}
return this.stackMin.peek();
}
}
public static class MyStack2{
private Stack<Integer> stackData;
private Stack<Integer> stackMin;
public MyStack2(){
this.stackData = new Stack<Integer>();
this.stackMin = new Stack<Integer>();
}
public void push(int newNum) {
if (this.stackMin.isEmpty()) {
this.stackMin.push(newNum);
}
else if (newNum < this.getmin()) {
this.stackData.push(newNum);
}
else {
int newMin = this.stackMin.peek();
this.stackMin.push(newMin);
}
this.stackData.push(newNum);
}
public int pop(){
if (this.stackData.isEmpty()) {
throw new RuntimeException("You stack is empty");
}
int value = this.stackData.pop();
if (value == this.getmin()) {
this.stackMin.pop();
}
return value;
}
public int getmin() {
if (this.stackMin.isEmpty()) {
throw new RuntimeException("You stack is empty");
}
return this.stackMin.peek();
}
}
}
题目2:
- 如何用栈结构实现队列结构
- 如何用队列结构实现栈结构
public class Code01_TwoStackImplementQueue {
public static class TwoStacksQueue{
public Stack<Integer> stackPush;
public Stack<Integer> stackPop;
public TwoStacksQueue() {
stackPush = new Stack<Integer>();
stackPop = new Stack<Integer>();
}
// push栈向pop栈导入数据
private void pushToPop(){
if (stackPop.empty()) {
while (!stackPush.empty()) {
stackPop.push(stackPush.pop());
}
}
}
public void add(int pushInt) {
stackPush.push(pushInt);
pushToPop();
}
public int poll() {
if (stackPop.empty() && stackPush.empty()) {
throw new RuntimeException("Queue is empty");
}
pushToPop();
return stackPop.pop();
}
public int peek() {
if (stackPop.empty() && stackPush.empty()) {
throw new RuntimeException("Queue is empty");
}
pushToPop();
return stackPush.peek();
}
}
}
(3)递归
栗子:
求数组arr[L…R]中的最大值,怎么用递归实现
- 将[L…R]范围分成左右两半,左:[L…Mid],右:[Mid+1…R]
- 左部分求最大值,右部分求最大值
- [L…R]范围上的最大值,是Max{左部分最大值,右部分最大值}
- 注意2是一个递归过程,当范围上只有一个数,就可以不用再递归啦
public class Code01_GetMax {
//求最大值
public static int getMax(int[] arr) {
return process(arr,0,arr.length-1);
}
private static int process(int[] arr,int L,int R){
if (L == R) {
return arr[L];
}
int mid = L + ((R - L) >> 2);
int leftMax = process(arr,L,mid);
int rightMax = process(arr,mid+1,R);
return Math.max(leftMax,rightMax);
}
public static void main(String[] args) {
int[] arr = {3,1,6,2,7,9,8,2};
System.out.println(getMax(arr));
}
}
(4)哈希表
哈希表在,增、删、改、查都是使用O(1)
Int 这种的类型是按值传递的;而大类型Integer是引用传递的,但是如果integer(-128~127)仍然是按值传递,非基础类型是引用传递。
总结:
- 哈希表增删改查可以认为是O(1)
- 放入哈希表的数据,如果是基础类型,内部按值传递,内存占用是这个数据的大小
- 放入哈希表的数据,如果不是基础类型,内部按引用传递,内存占用是8个字节
(5)有序表
7、二叉树的基本算法
(1)二叉树的先序、中序、后序遍历
- 先序:任何子树的处理顺序都是,先头节点、再左子树、然后右子树
- 中序:任何子树的处理顺序都是,先左子树、再头节点、然后右子树
- 后序:任何子树的处理顺序都是,先左子树、再右子树、然后头节点
递归方式实现二叉树的先序、中序、后序遍历
- 理解递归序
- 先序、中序、后序都可以在递归序基础上加工出来
- 第一次到达一个节点就打印就是先序、第二次打印即中序、第三次即后序
非递归方式实现二叉树的先序、中序、后序遍历
- 任何递归函数都可以改变成非递归
- 自己设计压栈的来实现
先序打印:1弹打印;2如有右,压入栈;3如有左,压入左
public static void pre(Node head) {
System.out.println("pre-order");
if (head != null) {
Stack<Node> stack = new Stack<>();
stack.add(head);
while (!stack.isEmpty()) {
head = stack.pop();
System.out.println(head.value + " ");
if (head.right != null) {
stack.push(head.right);
}
if (head.left != null) {
stack.push(head.left);
}
}
}
System.out.println();
}
中序打印:
public static void in(Node head) {
System.out.println("in-order");
if (head != null) {
Stack<Node> stack = new Stack<>();
while (!stack.isEmpty() || head != null) {
if (head != null) {
stack.push(head);
head = head.left;
}
else {
head = stack.pop();
System.out.println(head.value + " ");
head = head.right;
}
}
}
System.out.println();
}
实现二叉树的层次遍历
- 其实就是宽度优先遍历,用队列
- 可以通过设置flag变量的方式,来发现某一层的结束
public static void pos2(Node h) {
System.out.println("pos-order");
if (h != null) {
Stack<Node> stack = new Stack<>();
stack.push(h);
Node c = null;
while (!stack.isEmpty()) {
c = stack.pop();
if (c.left != null && h != c.right) {
stack.push(c.right);
}
else if (c.right != null && h != c.right) {
stack.push(c.right);
}
else {
System.out.println(stack.pop().value + " ");
h = c;
}
}
}
System.out.println();
}
题目:如何设计一个打印整棵树的打印函数
//中序
public class Code08_PaperFolding {
public static void printAllFolds(int N) {
printProcess(1,N,true);
}
private static void printProcess(int i,int N,boolean down) {
if (i > N) {
return;
}
printProcess(i+1,N,true);
System.out.println(down ? "凹": "凸");
printProcess(i+1,N,false);
}
public static void main(String[] args) {
int N = 3;
printAllFolds(N);
}
}
(2)二叉树套路
可以解决面试中绝大多数的二叉树问题尤其是树型dp问题,本质是利用递归遍历二叉树的便利性。
递归套路:
- 假设以X节点为头,假设可以向X左树和X右树要任何信息
- 在上一步的假设下,讨论以X为头节点的树,得到答案的可能性(最重要)
- 列出所有可能性后,确定到底需要向左树和右树要什么样的信息
- 把左树信息和右树信息求全集,就是任何一颗子树都需要返回的信息S
- 递归函数都返回S,每一颗子树都这么要求
- 写代码,在代码中考虑如何把左树的in 信息和右树信息整合出整颗树的信息
题目:给定一颗二叉树的头节点head,返回这颗二叉树是不是平衡二叉树
8、打表技巧和矩阵处理技巧
题目1:
小虎去买苹果,商店只提供两种类型的塑料袋,每种类型都有任意数量。
- 能装下6个苹果的袋子
- 能装下8个苹果的袋子
小虎可以自由使用两种袋子装苹果,但是小虎有强迫证,他要求自己使用的袋子数量必须最少,且使用的每个袋子必须装满。
给定一个正整数N,返回至少使用多少袋子。如果N无法让使用每个袋子必须装满,返回-1;
题目2:
给定一个正整数N,表示有N份青草统一推放在仓库里有一只牛和一只羊,牛先吃、羊后吃,它俩轮流吃草不管是牛还是羊,每一轮能吃的草量必须是:1、4、16、64 。。。(4的某次放)
谁最先把草吃完,谁获胜,假设牛和羊都绝顶聪明,都想赢,都会做出理性的决定,根据唯一的参数N,返回谁会赢
public class Code02_EatGrass {
public static String winner1(int n) {
if (n < 5) {
return (n == 0 || n == 2) ? "后手" : "先手";
}
int base = 1;
while (base <= n) {
if (winner1(n - base).equals("厚实")){
return "先手";
}
if (base > n / 4) {
break;
}
base *= 4;
}
return "后手";
}
public static void main(String[] args) {
for (int i = 0; i < 50; i++) {
System.out.println(i+":"+winner1(i));
}
}
}
题目3:
定义一种数:可以表示成若干(数量>1)连续正数,比如:
- 5 = 2 + 3,5就是这样的数
- 12 = 3 + 4 +5
- 1 不是这样的数,因为要求数量大于1、连续正数和
- 2 = 1 + 1,2也不是,因为等号右边不是连续正数
- 给定一个参数N,返回是不是可以表示成若干连续正数的数
public class Code03_MSumToN {
public static boolean isMSum1(int num) {
for (int i = 1; i <= num; i++) { // 开头的数i...
int sum = i;
for (int j = i + 1; j <= num; j++) {
if (sum + j > num) {
break;
}
if (sum + j == num) {
return true;
}
sum += j;
}
}
return false;
}
public static boolean isMSum2(int num) {
if (num < 3) {
return false;
}
return (num & (num - 1)) != 0;
}
public static void main(String[] args) {
for (int num = 1; num <= 5; num++) {
System.out.println(num + ":" + isMSum1(num));
}
for (int num = 1;num < 100;num++) {
System.out.println(num +":"+isMSum2(num));
}
}
}
题目4:zigzag打印矩阵
题目5:转圈打印矩阵
public class Code05_printMatrix {
public static void sprralOrderPrint(int[][] matri) {
int tR = 0;
int tC = 0;
int dR = matri.length - 1;
int dC = matri[0].length - 1;
while (tR <= dR && tC <= dC) {
printEdge(matri,tR++,tC++,dR--,dC--);
}
}
public static void printEdge(int[][] m,int a,int b,int c,int d) {
if (a == c) {
for (int i = b; i <= d; i++) {
System.out.print(m[a][i] + " ");
}
}
else if (b == d) {
for (int i = a; i <= c; i++) {
System.out.print(m[i][b]+" ");
}
}
else {
int curC = b;
int curR = a;
while (curC != d) {
System.out.print(m[a][curC] + "");
curC++;
}
while (curR != c) {
System.out.print(m[curR][d] + "");
curR++;
}
while (curC != b) {
System.out.print(m[c][curC] + "");
curC--;
}
while (curR != a) {
System.out.print(m[curR][b] + "");
curR--;
}
}
}
}
题目6:原地转正方法形矩阵
public class Code04_RotateMatrix {
public static void rotate(int[][] matrix) {
int a = 0;
int b = 0;
int c = matrix.length - 1;
int d = matrix[0].length - 1;
while (a < c) {
rotateEdge(matrix,a++,b++,c--,d--);
}
}
public static void rotateEdge(int[][] m,int a,int b,int c,int d) {
int tmp = 0;
for (int i = 0;i < d - b;i++) {
tmp = m[a][b+i];
m[a][b+i] = m[c-i][b];
m[c-i][b] = m[c][d-i];
m[c][d-i] = m[a+i][d];
m[a+i][d] = tmp;
}
}
public static void main(String[] args) {
int[][] arr = {{1,2,3,4,5},{6,7,8,9,10},{11,12,13,14,15},{16,17,18,19,20},{21,22,23,24,25}};
for (int[] x : arr) {
for (int y : x) {
System.out.print(y+" ");
}
System.out.println();
}
System.out.println("=======================");
rotate(arr);
for (int[] x : arr) {
for (int y : x) {
System.out.print(y+" ");
}
System.out.println();
}
}
}
9、并查集结构和图相关算法
贪心算法求解的标准过程
- 分析业务
- 根据业务逻辑找到不同的贪心策略
- 对于能举出反例的策略直接跳过,不能举出反例的策略要证明有效性,这往往是特别困难,要求数学能力很高且不具有统一的技巧性
贪心算法的解题套路:
- 实现一个不依靠贪心策略的解法X,可以用暴力的尝试
- 脑补出贪心策略A、贪心策略B、贪心策略C
- 用解法X和对数器,用实验的方式得知哪个贪心策略正确
- 不纠结贪心策略的证明
题目1:
一些项目要占用一个会议室宣讲,会议室不能同时容纳两个项目的宣讲。
给你每一个项目开始时间和结束时间,让你来安排宣讲的日程,要求会议室进行的宣讲的场次最多,返回最多的宣讲场次。
(1)并查集
- 有若干个样本ABCD。。。类型假设是V
- 在并查集中一开始认为每个样本都在单独的集合里
- 用户可以在任何时候调用如下两个方法
- boolean isSameSet(V x,V y):查询样本X和样本Y是否属于一个集合
- void union(V x,V y):把X和Y各自所在集合的所有样本合并成一个集合
- isSameSet 和 union方法的代价越低越好
题目2:
如果两个user,a字段一样、或者b字段一样、或者c字段一样,就认为是一个人。请结合users,返回合并之后的用户数量。
(2)图