本文为3月份准备面试学习左程云算法笔记记录,在此保存一下文章以便复习记忆。此文只涉及算法基础,更多内容请参考力扣与左程云算法课程。
时间复杂度
一个操作如果和样本的数据量没有关系,每次都是固定的时间内完成的操作,叫做常数操作。
时间复杂度为一个算法流程中,常数操作数量的一个指标。常用O(读作big O)来表示。具体来说,先要对一个算法操作流程非常熟悉,然后去写出这个算法流程中,发生了多少常数操作,进而总结出常数操作数量的表达式。
在表达式中,只要高阶项,不要低阶项,也不要高阶项的系数,剩下的部分如果为f(N),那么时间复杂度为O(f(N))。
评价一个算法的好坏,先看时间复杂度的指标,然后再去分析不同数据样本下的实际运行时间,也就是“常数项时间”。
递归如何计算时间复杂度
master公式
T(N) = a*T(N/B)+O(N^d)
- log(b,a) > d -----> 复杂度为O(N^log(b,a))
- log(b,a) = d -----> 复杂度为O(N^d * logN)
- log(b,a) < d -----> 复杂度为O(N^d)
a是调用几次递归函数,b是等量分解的分数,O(N^d)是除去递归额外的复杂度。
排序算法
异或
交换数组中两个位置的数
/**
* 交换数组中i和j位置上的两个数
*
* @param arr
* @param i
* @param j
*/
public static void swap(int[] arr, int i, int j) {
int temp;
temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
/* 异或交换数组两个位置的数
if (i == j) {
throw new RuntimeException("交换的位置i和j不能相等");
}
arr[i] = arr[i] ^ arr[j];
arr[j] = arr[i] ^ arr[j];
arr[i] = arr[i] ^ arr[j];
*/
}
^ 是异或,相同为0,不同为1
冒泡排序
/**
* 冒泡排序
*
* @param arr
*/
public static void bubbleSort(int[] arr) {
for (int i = 0; i < arr.length - 1; i++) {//趟数
for (int j = 0; j < arr.length - 1 - i; j++) {//每次比较j和j+1的数,把最大(最小)的数移到最右边
if (arr[j] > arr[j + 1]) {
swap(arr, j, j + 1);
}
}
}
}
选择排序
/**
* 选择排序
*
* @param arr
*/
public static void selectionSort(int[] arr) {
for (int i = 0; i < arr.length - 1; i++) {//0~arr.length - 1位置上每次找最大(最小)的数移到i位置上
for (int j = i + 1; j < arr.length; j++) {
if (arr[i] > arr[j]) {//升序
swap(arr, i, j);
}
}
}
}
二分算法(查找最大值)
/**
* 用二分与递归查找一个数组的最大值
* @param arr
* @return
*/
public static int getMax(int[] arr) {
return process(arr, 0, arr.length - 1);
}
/**
* 在l~r上查找最大值
*
* @param arr
* @param l
* @param r
* @return
*/
private static int process(int[] arr, int l, int r) {
if (l == r) {
return arr[l];
}
int mid = l + ((r - l) >> 1);
int leftMax = process(arr, l, mid);
int rightMax = process(arr, mid + 1, r);
return Math.max(leftMax, rightMax);
}
归并排序
/**
* 归并排序
* @param arr
*/
public static void mergeSort(int[] arr) {
if (arr == null || arr.length < 2) {
return;
}
mergeSortProcess(arr, 0, arr.length - 1);
}
private static void mergeSortProcess(int[] arr, int l, int r) {
if (l == r) {
return;
}
int mid = l + ((r - l) >> 1);
mergeSortProcess(arr, l, mid);
mergeSortProcess(arr, mid + 1, r);
merge(arr, l, mid, r);
}
/**
* 归并排序每一步的merge过程
* @param arr
* @param l
* @param mid
* @param r
*/
private static void merge(int[] arr, int l, int mid, int r) {
int[] help = new int[r - l + 1];
int i = 0;
/**
* 左右两个指针分别遍历
*/
int p1 = l;
int p2 = mid + 1;
while (p1 <= mid && p2 <= r) {
help[i++] = arr[p1] <= arr[p2] ? arr[p1++] : arr[p2++];
}
while (p1 <= mid) {
help[i++] = arr[p1++];
}
while (p2 <= r) {
help[i++] = arr[p2++];
}
for (i = 0; i < help.length; i++) {
arr[l + i] = help[i];
}
}
快速排序
/**
* 快速排序
* @param arr
*/
public static void quickSort(int[] arr) {
if (arr == null || arr.length < 2) {
return;
}
quickSortProcess(arr, 0, arr.length - 1);
}
public static void quickSortProcess(int[] arr, int L, int R) {
if (L < R) {
swap(arr, (int) (L + Math.random() * (R - L + 1)), R);//随机选取一个数作为划分值
int[] p = partition(arr, L, R);
quickSortProcess(arr, L, p[0] - 1);
quickSortProcess(arr, p[1] + 1, R);
}
}
/**
* 快速排序的分步过程
* @param arr
* @param L
* @param R
* @return
*/
public static int[] partition(int[] arr, int L, int R) {
int less = L - 1;
int more = R;
while (L < more) {
if (arr[L] < arr[R]) {//小于R上的数左移
swap(arr, ++less, L++);
} else if (arr[L] > arr[R]) {//大于R上的数右移
swap(arr, --more, L);
} else {//等于R上的数指针继续移动
L++;
}
}
swap(arr, more, R);//最后再把R上的数换到中间去
return new int[]{less + 1, more};
}
堆排序
public void sort(int[] arr) {
if (arr == null || arr.length < 2) {
return;
}
//这种构建大根堆略慢
//for (int i = 0; i < arr.length; i++) {//先将所有数添加进大根堆
// heapInsert(arr, i);//O(logN)
//}
//这种构建大根堆会快一些
for (int i = arr.length / 2 - 1; i >= 0; i--) {//先将所有数添加进大根堆
heapIfy(arr, i, arr.length);
}
int heapSize = arr.length;
swap(arr, 0, --heapSize);
while (heapSize > 0) {//再对大根堆每次删除最大值节点
heapIfy(arr, 0, heapSize);//O(logN)
swap(arr, 0, --heapSize);//将最大值节点放到后面
}
}
/**
* 将堆上index位置的数上移
*
* @param arr
* @param index
*/
private void heapInsert(int[] arr, int index) {
while (arr[index] > arr[(index - 1) / 2]) {
swap(arr, index, (index - 1) / 2);
index = (index - 1) / 2;
}
}
/**
* 将堆上index位置的数下移
*
* @param arr
* @param index
* @param heapSize
*/
private void heapIfy(int[] arr, int index, int heapSize) {
int left = index * 2 + 1;//左孩子下标
while (left < heapSize) {
//两个孩子相比
int largest = left + 1 < heapSize && arr[left + 1] > arr[left] ? left + 1 : left;
//父亲结点跟大的孩子节点相比
largest = arr[largest] > arr[index] ? largest : index;
if (largest == index) {//说明当前节点已经是最大了
break;
}
swap(arr, largest, index);
index = largest;
left = index * 2 + 1;
}
}
桶排序
public void sort(int[] arr) {
if (arr==null||arr.length<2){
return;
}
radixSort(arr,0,arr.length-1,maxBits(arr));
}
/**
* 桶排序
* @param arr
* @param L
* @param R
* @param digit
*/
private void radixSort(int[] arr, int L, int R, int digit) {
final int radix = 10;
int i, j;
//辅助空间
int[] bucket = new int[R - L + 1];
for (int d = 1; d <= digit; d++) {//有多少位就进出桶多少次
//10个桶
//count[0] 当前d位是0的数字有多少个
//count[1] 当前d位是0和1的数字有多少个
//....
int[] count = new int[radix];
for (i = L; i <= R; i++) {//将d为上的每个数计数
j = getDigit(arr[i], d);
count[j]++;
}
for (i = 1; i < radix; i++) {//计算前缀和
count[i] = count[i] + count[i - 1];
}
for (i = R; i >= L; i--) {//出桶
j = getDigit(arr[i], d);
bucket[count[j] - 1] = arr[i];
count[j]--;
}
for (i = L, j = 0; i <= R; i++, j++) {//将辅助空间复制给原始数组
arr[i] = bucket[j];
}
}
}
/**
* 获取一个数是几位数
*
* @param x
* @param d
* @return
*/
private int getDigit(int x, int d) {
return ((x / ((int) Math.pow(10, d - 1))) % 10);
}
/**
* 获取数组上最长的位数
* @param arr
* @return
*/
private int maxBits(int[] arr){
int max = Integer.MIN_VALUE;
for (int i=0;i<arr.length;i++){
max = Math.max(max,arr[i]);
}
int res=0;
while (max!=0){
res++;
max/=10;
}
return res;
}
排序算法总结
链表
回文链表
判断一个链表是否为回文链表
方法一:用额外辅助空间栈,快慢指针法将链表后半部分压入栈。再从头遍历链表,同时弹出栈顶元素,都一样即为回文链表
方法二:不用辅助空间,快慢指针法将后半部分链表反转,两个指针分别从头和尾遍历,都一样为回文链表,最后再恢复反转的链表。
划分链表
将一个链表以某一个值做划分,小于的放左边,等于的放中间,大于的放右边。
code:
public static Node partition(Node head, int pivot) {
Node sH = null; // 小于部分结点的头
Node sT = null; // 小于部分结点的尾
Node eH = null; // 等于部分结点的头
Node eT = null; // 等于部分结点的尾
Node mH = null; // 大于部分节点的头
Node mT = null; // 大于部分结点的尾
Node next; // 保存下一个结点
while (head != null) {
next = head.next;
head.next = null;
if (head.val < pivot) {//连接小端
if (sH == null) {
sH = head;
} else {
sT.next = head;
}
sT = head;
} else if (head.val == pivot) {//连接中端
if (eH == null) {
eH = head;
} else {
eT.next = head;
}
eT = head;
} else {//连接大端
if (mH == null) {
mH = head;
} else {
mT.next = head;
}
mT = head;
}
head = next;
}
//小尾连中头,中尾连大头
if (sT != null) {
sT.next = eH;
eT = eT == null ? sT : eT;
}
if (eT != null) {
eT.next = mH;
}
return sH != null ? sH : (eH != null ? eH : mH);
}
复制链表
剑指 Offer 35. 复杂链表的复制 - 力扣(LeetCode)
环形链表
找到环形链表第一个入环结点
public static Node getLoopNode(Node head) {
if (head == null || head.next == null || head.next.next == null) {//少于三个节点不会成环
return null;
}
Node slow = head.next;
Node fast = head.next.next;
while (fast!=slow){//快慢指针相遇代表有环
if (fast.next==null||fast.next.next==null){
return null;
}
fast=fast.next.next;
slow=slow.next;
}
fast=head;
while (fast!=slow){//再次相遇即为入环结点
fast=fast.next;
slow=slow.next;
}
return slow;
}
找两个链表的公共部分
剑指 Offer 52. 两个链表的第一个公共节点 - 力扣(LeetCode)
二叉树
遍历
递归
public static void printTree(Node head) {
if (head == null) {
return;
}
// 先序遍历
System.out.println("先序遍历:"+head.val);
printTree(head.left);
// 中序遍历
//System.out.println("中序遍历:"+head.val);
printTree(head.right);
// 后序遍历
//System.out.println("后序遍历:"+head.val);
}
迭代
public static void printTreePre(Node head) {
Deque<Node> stack = new ArrayDeque<>();
stack.push(head);
//先序遍历,将头结点入栈,每次弹出时,将右结点先压入,再压左结点
//周而复始,每次弹出的值就是先序遍历
while (!stack.isEmpty()){
Node pop = stack.pop();
System.out.println("先序遍历:"+pop.val);
if (pop.right!=null){
stack.push(pop.right);
}
if (pop.left!=null){
stack.push(pop.left);
}
}
}
public static void printTreeLast(Node head){
Deque<Node> stack = new ArrayDeque<>();
Deque<Node> collect = new ArrayDeque<>();
stack.push(head);
//后序遍历,先序遍历是头左右,将压栈顺序改为先压左节点,再压右节点就是头右左的顺序
//再反转就是左右头,即为后序遍历
while (!stack.isEmpty()){
Node pop = stack.pop();
collect.push(pop);
if (pop.left!=null){
stack.push(pop.left);
}
if (pop.right!=null){
stack.push(pop.right);
}
}
while (!collect.isEmpty()){
System.out.println("后序遍历:"+collect.pop().val);
}
}
public static void printTreeIn(Node head){
Deque<Node> stack = new ArrayDeque<>();
// 中序遍历,先将所有的左结点入栈,再依次弹出,每次弹出如果有右节点就再将右节点上的左节点入栈
// 每次弹出的值就是中序遍历
while (!stack.isEmpty()||head!=null){
if (head!=null){
stack.push(head);
head=head.left;
}else {
head=stack.pop();
System.out.println("中序遍历:"+head.val);
head=head.right;
}
}
}
由前序遍历,中序遍历推导二叉树
思路:前序遍历结构:根节点 | 左节点 | 右结点
中序遍历:左节点 | 根节点 | 右节点
代码:
class Solution {
HashMap<Integer, Integer> map = new HashMap<>();//标记中序遍历
int[] preorder;//保留的先序遍历,方便递归时依据索引查看先序遍历的值
public TreeNode buildTree(int[] preorder, int[] inorder) {
this.preorder = preorder;
//将中序遍历的值及索引放在map中,方便递归时获取左子树与右子树的数量及其根的索引
for (int i = 0; i < inorder.length; i++) {
map.put(inorder[i], i);
}
//三个索引分别为
//当前根的的索引
//递归树的左边界,即数组左边界
//递归树的右边界,即数组右边界
return recur(0,0,inorder.length-1);
}
TreeNode recur(int pre_root, int in_left, int in_right){
if(in_left > in_right) return null;// 相等的话就是自己
TreeNode root = new TreeNode(preorder[pre_root]);//获取root节点
int idx = map.get(preorder[pre_root]);//获取在中序遍历中根节点所在索引,以方便获取左子树的数量
//左子树的根的索引为先序中的根节点+1
//递归左子树的左边界为原来的中序in_left
//递归左子树的右边界为中序中的根节点索引-1
root.left = recur(pre_root+1, in_left, idx-1);
//右子树的根的索引为先序中的 当前根位置 + 左子树的数量 + 1
//递归右子树的左边界为中序中当前根节点+1
//递归右子树的右边界为中序中原来右子树的边界
root.right = recur(pre_root + (idx - in_left) + 1, idx+1, in_right);
return root;
}
}
广度优先遍历
public static void getNodeByWidth(Node head) {
Queue<Node> queue = new LinkedList<>();
queue.add(head);
while (!queue.isEmpty()) {//先压左再压右,保证每层从左往右遍历
Node poll = queue.poll();
if (poll.left != null) {
queue.add(poll.left);
}
if (poll.right != null) {
queue.add(poll.right);
}
}
}
每层遍历
public static void getNodeByWidthLevel(Node head) {
Queue<Node> queue = new LinkedList<>();
Map<Node,Integer> map = new HashMap<>();
map.put(head,1);
queue.add(head);
while (!queue.isEmpty()) {//先压左再压右,保证每层从左往右遍历
int size = queue.size();//每层节点数量
for (int i=0;i<size;i++){//这里的for就是对每一层进行遍历,并将下一层进入队列
Node poll = queue.poll();
if (poll.left != null) {
queue.add(poll.left);
map.put(poll.left,map.get(poll)+1);
}
if (poll.right != null) {
queue.add(poll.right);
map.put(poll.right,map.get(poll)+1);
}
}
}
}
搜索二叉树检查
中序遍历二叉树,一直保持递增就为搜索二叉树,否则不是。
public static boolean checkBst(Node head,int preValue){
if (head==null){
return true;
}
boolean left = checkBst(head.left, preValue);
if (!left){
return false;
}
if (preValue>=head.val){//中序遍历检查值是否递增
return false;
}else {
preValue = head.val;
}
return checkBst(head.right,preValue);
}
判断是否是完全二叉树
层序遍历,满足两个条件:1.任一节点不能只有右孩子没有左孩子 2.遇到第一个左右孩子不全的节点,后续所有节点不能有孩子节点。
判断是否是满二叉树
遍历获取二叉树的深度H和总结点个数N。满足2^H-1=N即为满二叉树。
判断是否是平衡二叉树
定义:左右两子树高度相差不能大于1,并且左右子树也满足平衡二叉树定义。
static class ReturnType{
boolean isBalance;
int height;
ReturnType(boolean first,int second){
isBalance = first;
height = second;
}
}
public ReturnType checkBalance(Node head) {
if (head==null){
return new ReturnType(true, 0);
}
ReturnType left = checkBalance(head.left);
ReturnType right = checkBalance(head.right);
int height = Math.max(left.height, right.height) + 1;//此节点的高度
boolean isBalance = left.isBalance&& right.isBalance&&(Math.abs(left.height)- right.height)<2;
return new ReturnType(isBalance, height);
}
树形DP
特性:左右子树标准要一致。例如搜索二叉树,满二叉树,平衡二叉树
将整体转化为一个结点的特性。例:判断是否是搜索二叉树,递归调用,在每个节点判断左右子树是否满足搜索二叉树。判断是否是满二叉树,递归后续遍历,判断左右子树是否满足满二叉树..........
比如满二叉树的判断:
求最低公共祖先
树形dp法
/**
* 求两个节点的最低公共祖先
*/
public static Node getLowestAncestor(Node head, Node o1, Node o2) {
//找到o1或者o2返回,没找到返回空值
if (head == null || head == o1 || head == o2) {
return head;
}
Node left = getLowestAncestor(head.left, o1, o2);
Node right = getLowestAncestor(head.right, o1, o2);
//左右都不为空说明找到了o1和o2,返回当前节点
if (left != null && right != null) {
return head;
}
//返回o1或者o2
return left != null ? left : right;
}
还有一种是用map记录每一个节点的父节点,然后通过map一直查找o1的父节点并存到set里面,直到head,同样的方式遍历o2的父节点,将o2的父节点加入set里面,当添加失败时就找到了他们的最低公共祖先。
n次折叠纸条,打印折痕方向
二叉数中序遍历
前缀树
一个字符串类型的数组arr1,另一个字符串类型的数组arr2。arr2中有哪些字符,是arr1中出现的?请打印。arr2有哪些字符,是作为arr1中某个字符串前缀出现的?请打印。arr2中有哪些字符,是作为arr1中某个字符串前缀出现的?请打印arr2中出现次数最大的前缀。
节点代码:
public static class TrieNode {
//有多少点经过这里
public int path;
//有多少点依此为终点
public int end;
public TrieNode[] nexts;
public TrieNode() {
path = 0;
end = 0;
//26代表底下可能可以连26个字母 如nexts[0]!=null 该字母有通向a的路
nexts = new TrieNode[26];
}
}
当有字母经过该点时,path++,如果是尾节点,end++;
e等于零表示不存在这样的字符串。例如若想知道有多少字符串以ab为前缀,可以看ab结点上的p值
构建前缀树:
public void insert(String word) {
if (word == null) {
return;
}
char[] chs = word.toCharArray();
TrieNode node = root;
int index = 0;
for (int i = 0; i < chs.length; i++) {
index = chs[i] - 'a';
if (node.nexts[index] == null) {
node.nexts[index] = new TrieNode();
}
node = node.nexts[index];
node.path++;
}
node.end++;
}
查找指定字符串:
public int search(String word) {
if (word == null) {
return 0;
}
char[] chs = word.toCharArray();
TrieNode node = root;
int index = 0;
for (int i = 0; i < chs.length; i++) {
index = chs[i] - 'a';
if (node.nexts[index] == null) {
return 0;
}
node = node.nexts[index];
}
return node.end;
}
查找有多少个字符以指定字符串为前缀:
public int prefixNumber(String pre) {
if (pre == null) {
return 0;
}
char[] chs = pre.toCharArray();
TrieNode node = root;
int index = 0;
for (int i = 0; i < chs.length; i++) {
index = chs[i] - 'a';
if (node.nexts[index] == null) {
return 0;
}
node = node.nexts[index];
}
return node.path;
}
贪心算法
大根堆、小根堆的应用。
哈夫曼树构造。
在数据流中,随时取得中位数。
-
将第一个数字放进大根堆
-
判断第二个数是否小于大根堆堆顶,若是,入大根堆,否则进小根堆
-
判断大小根堆的大小,如果大size-小size>=2,较大堆顶弹出进较小的堆 如以下的流程
-
5先进入大根堆,3小于5进大根堆,此时大根堆的长度超过小根堆的长度超过2,将5弹出并放进小根堆
-
7和4都大于3放进小根堆,此时小根堆的长度超过大根堆的长度超过2,将4弹出并放进大根堆 此时较小的中位数在大根堆中,较大的中位数在小根堆中 整个流程的所有调整都为logN水平
N皇后问题
public static int num1(int n) {
if (n < 1) {
return 0;
}
int[] record = new int[n];
return process1(0, record, n);
}
// 目前来到了第i行,record[0...i-1]之前的行放过的皇后 整体一共有n行 返回值为合理的摆法
public static int process1(int i, int[] record, int n) {
//终止行
if (i == n) {
return 1;
}
int res = 0;
for (int j = 0; j < n; j++) {
if (isValid(record, i, j)) {
record[i] = j;
res += process1(i + 1, record, n);
}
}
return res;
}
public static boolean isValid(int[] record, int i, int j) {
for (int k = 0; k < i; k++) {
// Math.abs(record[k] - j) == Math.abs(i - k)判断两点是否共斜线 即45度
if (j == record[k] || Math.abs(record[k] - j) == Math.abs(i - k)) {
return false;
}
}
return true;
}
递归相关
汉诺塔问题:
public static void hanoi(int n) {
if (n > 0) {
func(n, n, "left", "mid", "right");
}
}
public static void func(int rest, int down, String from, String help, String to) {
if (rest == 1) {
System.out.println("move " + down + " from " + from + " to " + to);
} else {
func(rest - 1, down - 1, from, to, help);
func(1, down, from, help, to);
func(rest - 1, down - 1, help, from, to);
}
}
public static void main(String[] args) {
int n = 3;
hanoi(n);
}
打印字符串子序列
public static void process(char[] chs, int i, List<Character> res) {
if(i == chs.length) {//一条分支走完
printList(res);
return;
}
List<Character> resKeep = copyList(res);
resKeep.add(chs[i]);
process(chs, i+1, resKeep);//左走
List<Character> resNoInclude = copyList(res);
process(chs, i+1, resNoInclude);//右走
}
打印符串全排列
public static void process(char[] chs, int i, ArrayList<String> res) {
if (i == chs.length) {//排列完一次
res.add(String.valueOf(chs));
}
boolean[] visit = new boolean[26];
for (int j = i; j < chs.length; j++) {
swap(chs, i, j);
process(chs, i + 1, res);
swap(chs, i, j);
}
}
不重复
public static void process(char[] chs, int i, ArrayList<String> res) {
if (i == chs.length) {
res.add(String.valueOf(chs));
}
boolean[] visit = new boolean[26];
for (int j = i; j < chs.length; j++) {
if (!visit[chs[j] - 'a']) {
visit[chs[j] - 'a'] = true;
swap(chs, i, j);
process(chs, i + 1, res);
swap(chs, i, j);
}
}
}