leetcode刷题总结

数据结构与算法代码实现

数组

联想关键词

  • 排序
  • 双指针、三指针
  • 扫描方向(左->右、右->左)

1. 遍历时需要用到其余位置的值,或者该位置的值此前是否被访问过

​ 可以考虑使用HashMap进行存储(哈希表时间复杂度为1),key或者value进行存储值,map中没有则put进去,有的话就取出来使用。

Map<Integer,Integer> map = new HashMap<Integer, Integer>();
int length = nums.length;
for (int i = 0; i < length; i++) {
//value与nums[i]有关
if (map.containsKey(value)) {
//map.get(value)就为索引下标,value为值
}
//没有就put进去
map.put(nums[i],i);
}

2. 数组题,一般就是左到右或者右到左扫描,通常我们就需要使用双指针,甚至三指针

​ 例如颜色分类排序中,head指针把将小的放前面,tail指针把大的放后面,cur指针从头到尾来扫描

int head = 0;
int tail = nums.length - 1;
int cur = head;
// cur等于tail时
while (cur <= tail) {
if (nums[cur] == 0) {
swap(nums, cur, head);
// 此时前面只能是0,1所以不需要再进行判断是否为2
head++;
cur++;
} else if (nums[cur] == 2) {
swap(nums, cur, tail);
tail--;
} else {
cur++;
}
}

3. 可以先排序,再进行求解

​ 例如三数之和中,先排序,再依次找两个数相加,少了往右走,多了就往左走

for (int i = 0; i <= nums.length - 3; i++) {
if (i > 0 && nums[i] == nums[i - 1]) {
continue;
}
int l = i + 1;
int r = nums.length - 1;
while (l < r) {
int sum = nums[l] + nums[r];
if (sum == -nums[i]) {
List<Integer> list = new ArrayList<>();
list.add(nums[i]);
list.add(nums[l]);
list.add(nums[r]);
resultList.add(list);
// 如果同时相减,可以会有重复结果,则需要去重复
// 例如:[-2,0,0,2,2]
while (l < r && nums[l] == nums[l + 1]) l++;
while (l < r && nums[r] == nums[r - 1]) r--;
l++;
r--;
} else if (sum < -nums[i]) {
l++;
} else {
r--;
}

}
}

4. 求两个有序数组,有序合并后第k大个数

  • 即使不是有序的,也可以先排序
  • 这类题也跟中位数差不多,也是求第几个大的数

可以使用分治/或者说是二分思想,使得时间复杂度到O(log (m+n))。

(1)找第k大数,两数组有序,则可以每次排除其中一个数组k / 2个数,或者整个数组

(2)如何确定排除哪个数组,则看边界上的值。

if (nums1[begin1 + mid - 1] <= nums2[begin2 + mid - 1])

如果数组临界k/2的值比另一个数组边界值要小一些,则前面的都要小,可以排除,begin往前走

// 递归退出条件(数组个数减少为0,以及k/2了为1)
if (m == 0) {
return nums2[begin2 + k - 1];
}
if (k == 1) {
return Math.min(nums1[begin1], nums2[begin2]);
}
// 排除掉 其中一个数组前k / 2 个数或者整个数组个数
int mid = Math.min(m, k / 2);
if (nums1[begin1 + mid - 1] <= nums2[begin2 + mid - 1]) {
begin1 = begin1 + mid;
} else {
begin2 = begin2 + mid;
}
return findKth(nums1, begin1, end1, nums2, begin2, end2, k - mid);

链表

链表结题技巧

  • 虚拟头结点
  • 快慢指针(求中间节点)
  • 双指针
  • 链表翻转
  • 检验边界情况:头、尾,传入一个节点进来处理,处理全部的节点

1. 双链表长度不一需要对位比较或者处理

​ 两个数用链表存储例如345(3->4->5)和6789(6->7->8->9)相加,则将短的链表高位空位置为0

​ 例:345(0->3->4->5)

int x = l1 == null ? 0 : l1.val;
int y = l2 == null ? 0 : l2.val;

​ 并且虚拟头结点newHead保存结果链表的开头,newHead的next才开始存储数据,然后 return newHead.next; 或者newHead.next开始遍历

ListNode newHead = new ListNode(0);
ListNode newTail = newHead; // 用newTail来指向链表尾部,且从newTail开始存数据

return newHead.next;

2. 链表分隔

​ 以某一值x,来分割链表,所有小于 x 的节点排在大于或等于 x 的节点之前,链表中包含 x,x 只需出现在小于 x 的元素之后。

思路:只需要使用双指针,类似于荷兰国旗题,小的挂在左尾指针后,大的挂在右尾指针后,最后连接左右两条链表即可。

ListNode leftTail = leftHead;
ListNode rightTail = rightHead;

连接左右两条链表

leftTail.next = rightHead.next;
rightTail.next = null;

3. 反转链表(默写)

  • 递归

思路:递归的思路很简单,就是把除了这个节点后面的反转后,然后指向这个节点,一直到最后一个节点,或者传进来就是null。

// 3. 设置边界条件,递归到一个的时候,或者传进来是null了,直接返回
if (head == null || head.next == null) {
return head;
}

// 1. 后面整体反转:head.next为反转前的头,反转后的尾,新头是newHead
ListNode newHead = reverseList(head.next);

// 2. 整体(反转后的尾)指向节点,所以就不能用newHead来指向
head.next.next = head;
head.next = null;

return newHead;
  • 迭代

思路:只有一个head能用,所以就只能从head入手一个一个串起来,加一个指针newhead来实现。

ListNode newhead = null;
//head为null了,就不用了,因为开始newhead已经是null了
while (head != null) {
// 先保存后一个节点位置,避免丢失
ListNode temp = head.next;

// head指向新的节点(开始为null)
head.next = newhead;
newhead = head;

// head迭代
head = temp;
}

4. 快慢指针(解决有环、对称、中间节点的问题)

4.1 解决是否有环问题

​ 顾名思义即一个走一步,而另一个走两步。low = low.next; fast = fast.next.next;

​ 注意:

  1. while (fast != null && fast.next != null),在while条件是加个fast.next的判断,避免null.next情况出现
  2. 开始最好设置low为一个head,而另一个fast为下一位,这样才能使我们low == fast只在有环时成立
// 对head进行入参检验(一个节点或者为null)
if (head == null || head.next == null) {
return false;
}

// 开始最好设置一个head,而另一个为下一位,这样才能使我们low == fast只在有环时成立
ListNode low = head;
ListNode fast = head.next;

while (fast != null && fast.next != null) {
if (low == fast) {
return true;
}

low = low.next;
fast = fast.next.next;
}

return false;

4.2 对称问题(求中间节点,默写)

ListNode slow = head;
ListNode fast = head.next;

while (fast != null && fast.next != null) {
fast = fast.next.next;
slow = slow.next;
}
return slow; //slow即中间节点

5. 遍历时该位置的值此前是否被访问过,或者需要用到其余位置的值

​ 之前我们在数组时有这种思想采用hash表实现,时间复杂度为O(1)

判断链表是否有环,我们基本上能想到就是快慢指针,但是其实我们只需遍历一遍判断 某个位置的值此前被访问过 即可判断有环,因为链表没有索引,所以就用hash表实现的列表(无重复的值)HashSet。

// 用hash表实现的列表set进行存储并校验
Set<ListNode> set = new HashSet<>();
ListNode node = head;

// node.next != null没有到尾部,这样做少判断一次
while (node.next != null) {
// 该位置的值此前有被访问过就知道有环
if (set.contains(node)) {
return true;
}
// 该位置的值此前没有被访问过就加入进去
set.add(node);
node = node.next;
}

return false;

6. while循环遍历,条件是head.next != null 还是 head != null

​ 主要是看后面循环体里面是否有head的next相关的赋值,保证没有null.next

例如:这里不仅有fast.next而且还有fast.next.next,所以就两个条件都要满足

while (fast != null && fast.next != null) {
......
fast = fast.next.next;
}

栈、队列

联想关键词

  • 栈:对称问题
  • 队列:按照顺序解决问题

1. 栈实现二叉树非递归遍历

​ 非递归进行前序、中序、后序遍历,条件为:一直向左走,访问左节点,然后回来访问根结点。所以就可以用栈来实现

Node<E> node = root;
Stack<Node<E>> stack = new Stack<>();
// 循环node = node.left;
while (true) {
if (node != null) {
// 访问器返回true就停止
if (visit(node.element)) {
return;
}
// 有右子节点就入栈
if (node.right != null) {
stack.push(node.right);
}
node = node.left;
} else if (stack.isEmpty()){
return;
} else {
// 右子节点开始遍历
node = stack.pop();
}
}

2. 单调栈实现某值左右(或者左右其中一个)第一个最值

要通过遍历,使得找到某值左右(或者左右其中一个)的第一个最值,可以使用单调栈

例如:找到最大二叉树的每个节点的父节点索引

Stack<Integer> stack = new Stack<>();
for (......) {
if (!stack.isEmpty() && nums[i] < nums[stack.peek()]) {
//nums[i] < 栈中元素,遇见小的了,设置左大
leftIndexes[i] = stack.peek();
}

while (!stack.isEmpty() && nums[i] > nums[stack.peek()]) {
//nums[i] > 栈中元素,遇见大的了,设置右大
rightIndexes[stack.peek()] = i;
}
stack.push(i);
}

3. 单调双端队列实现一段区间内的最大、最小值和排序

要通过遍历,使得比它大(小)的值在最前面取,小(大)的值删也是删前面的,添加都是放后面的,所以遍历就保证了单调递减(递增),从而找到这个队列区间的元素的最值。

例:滑动的窗口的最值问题

for (......) {
// 删除队列中索引对应元素值小的
while (!deque.isEmpty() && nums[deque.peekLast()] <= nums[i] ) {
deque.pollLast();
}
// 将该索引入队
deque.offerLast(i);
//...............................................................
// 设置最大值
maxes[w] = nums[deque.peekFirst()];
}

字符串

1. Java语言方面

​ String是个字符对象,其引用时指向字符串常量池中的一个字符数组char数组,所以最好每次先把String转成char[],并且长度附一个变量,不用每次都调用方法。例如:

int length = s.length();
char[] chars = s.toCharArray();

2. 循环移位子串

​ 某一部分相等,字符串的子串问题,且发现子串是循环移位且满足时钟周期的特点,所以拼接多个时钟周期(比如两个原串)。

比如water -> aterw,waterwater中就出现了子串

3. 树找子树

​ 树找子树,很显然就是遍历出来,找到子树(子串),就类似于字符串寻找子串的问题,所以只需将其序列化成字符串。

注意:树序列化过程中,例如 !3!4!1!#!#!2!#!#!5 里面找 !4!1!#!#!2!#!#

(1) 最好用特殊符号来表示null,并且用再用一个字符隔开,不然出现问题

(2) 就转换成了一个找子串的问题

二叉树

联想关键词

树的题目一般都是通过:

  • 递归
  • 前、中、后、层序遍历

1. 完全二叉树性质

​ 完全二叉树的n1要么是0,要么是1(节点按序号排列),则由通用性质:n = 2n0 + n1 - 1

  • n1为0,则n = 2n0 - 1,n为奇数,叶子节点n0 =(n + 1)/ 2,非叶子节点n1 + n2 = (n - 1)/ 2
  • n1为1,则n = 2n0,n为偶数,叶子结点n0 = n / 2,非叶子节点n1 + n2 = n / 2

2. 二叉树遍历应用

  • 前序遍历:树状结构的展示(注意左右子树的顺序)
  • 中序遍历:二叉搜索树的中序遍历按升序或者降序处理节点
  • 后序遍历:适用于一些先子后父的操作
  • 层序遍历:计算二叉树的高度,判断一棵树是否为完全二叉树

3. 判断二叉树结构

​ 可以采用序列化成字符串,即前、中、后序遍历成字符串,加上!和 # 来作为分隔和空节点再采用字符串的算法,进行匹配,相等。

4. 二叉搜索树BST

​ 看到这个敏感词二叉搜索树,一般不出意外就要用到它的性质:中序遍历是个升序的序列,或者递归判断是否为BST树(左右是否为,根大于左,小于右)。

递归 + 回溯

1. 二叉树

一般链表或者二叉树的问题都可以使用递归的思想,因为链表和二叉树的结构,本身就是递归的结构,即二叉树里面还是二叉树。

**树的题都是递归 + 遍历解决。**

例如:寻找最大的BST子树

public int largestBSTSubtree(TreeNode root) {
if (root == null) {
return 0;
}
if (isBST(root)) {
return nodesCount(root);
}
// 否则就返回左右子树中最多的
return Math.max(largestBSTSubtree(root.left), largestBSTSubtree(root.right));
}

2. DFS

DFS的递归题目:排列组合,解决方法:递归 + 回溯

例如:求二叉树的路径总和等于某值时

public List<List<Integer>> pathSum(TreeNode root, int sum) {
List<List<Integer>> list = new ArrayList<>();
// 返回空集合,通过leetcode判题结果得出
if (root == null) {
return list;
}

// 进入第一层
dfs(root, sum, new ArrayList<>(), list);
return list;
}

private void dfs(TreeNode node, int sum, List<Integer> result, List<List<Integer>> list) {
// 假设进入最后一层,不能再深入
if (node == null) {
return;
}

// sum迭代将val存起来
sum -= node.val;
result.add(node.val);
// 判断node的左右,简洁一下有一个不为null都进入
if (node.left == null && node.right == null) {
if (sum == 0) {
list.add(new ArrayList<>(result));
}
} else {
dfs(node.left, sum, result, list);
dfs(node.right, sum, result, list);
}
// 还原
result.remove(result.size() - 1);
}

3. 回溯

​ 树和图的DFS就是典型的回溯,而且回溯本质就是使用递归来实现,例如八皇后和走迷宫。

经典N皇后问题优化

回溯 + 剪枝处理,剪枝即剪去肯定错误的位置时(同一列,在其对角线上),使用三个标记数组来实现。

以空间换时间,不用每次都用O(n)复杂度

for (int col = 0; col < n; col++) {
// 剪枝,如果判断不在该列,不在对角线,可以摆
if (cols[col]) {
continue;
}
// 通过行和列计算,所在对角线索引leftIndex 和 rightIndex
int leftIndex = n - 1 - col + row;
int rightIndex = row + col;
if (leftTop[leftIndex] || rightTop[rightIndex]) {
continue;
}

// 在第row行第col列摆放皇后
queens[row] = col;
cols[col] = true;
leftTop[leftIndex] = true;
rightTop[rightIndex] = true;
// 继续下一行
place(row + 1);

// 该行所有都不满足,place(该行)执行完毕,回溯到上一行
// 刚刚标记的需要重制
cols[col] = false;
leftTop[leftIndex] = false;
rightTop[rightIndex] = false;
}
针对特定的数值(例如八皇后)每一位只有两种结果,可以使用位运算来优化(存在为1,不存在为0)
/**
* 标记所有列是否摆放皇后
* 采用位运算优化,8位二进制数 = byte
*/
byte cols;

/**
* 标记所有主对角线是否摆放皇后
* leftTop[1] = true,则第一条对角线是否摆放皇后
*/
short leftTop;

/**
* 标记所有副对角线是否摆放皇后
* rightTop[1] = true,则第一条对角线是否摆放皇后
*/
short rightTop;

/*--------------------剪枝操作--------------------------*/
if ((cols & (1 << col)) != 0) {
continue;
}
// 通过行和列计算,所在对角线索引leftIndex 和 rightIndex
int leftIndex = n - 1 - col + row;
int rightIndex = row + col;

if ((leftTop & (1 << leftIndex)) != 0 ||
(rightTop & (1 << rightIndex)) != 0) {
continue;
}
// 摆放
cols = (byte)(cols | (1 << col));
leftTop = (byte)(leftTop | (1 << leftIndex));
rightTop = (byte)(rightTop | (1 << rightIndex));
// 刚刚标记的需要重制
// 01111101 n
//&11111011 ~00000100
// 01111001
cols = (byte)(cols & ~(1 << col));
leftTop = (byte)(leftTop & ~(1 << leftIndex));
rightTop = (byte)(rightTop & ~(1 << rightIndex));

4. 递归转迭代

递归是从大的值推出小的值,例如f(10)-> f(9),但是如果我们知道递推关系式,且初始值知道,就可以从小一层一层推出大的值,就可以迭代。例如斐波那契数列、约瑟夫环问题(递推公式法)。
/**
* f(n,m) = (f(n - 1,m) + m) % n
* 已知一些初始值(前面的)和递推式,需要顶层的值,则迭代求
* @param n
* @param m
* @return
*/
public int lastRemaining1(int n, int m) {
// 一个人,要返回第m个,那就是这个人编号为0
if (n == 1) {
return 0;
}
int result = 0;
for (int i = 2; i <= n; i++) {
result = (result + m) % i;
}
return result;
}

动态规划

动态规划一般求解最优问题的一种算法策略,后面的解与前面的状态有关,步骤为三步:
  1. 定义dp[i]的含义

  2. 设置初始状态(边界)

  3. 确定状态转移方程:dp[i]与dp[i - 1]的关系

联想关键词

  • 最值问题

  • 你发现从某一步变到某一步,有多种方式,这让你的思路踌躇两难不知道什么时候是最优解,而且这一步的变化只跟前一步的有关。

1. dp[i]对于数组、字符序列来说,一般定为以该值(nums[i])结尾的满足条件的序列

例如:求最长上升(递增)子序列的长度,dp为以nums[i]结尾的...长度
dp[0] = 1;
int maxLength = 1;
for (int i = 1; i < nums.length; i++) {
dp[i] = 1;
for (int j = i - 1; j >= 0 ; j--) {
if (nums[i] > nums[j]) {
// 如果dp[j] + 1大于上一次赋的值dp[i]
dp[i] = Math.max(dp[i], dp[j] + 1);
}
}
// 取每次循坏的最大值
maxLength = Math.max(dp[i], maxLength);
}

2. dp[i,j]还可以是多元的,例如两个数组序列的公共问题

int[][] dp = new int[2][nums2.length + 1];
for (int i = 1; i <= nums1.length; i++) {
// 前一行和当前行
int prevRow = (i - 1) & 1;
int row = i & 1;
for (int j = 1; j <= nums2.length; j++) {
if (nums1[i - 1] == nums2[j - 1]) {
dp[row][j] = dp[prevRow][j - 1] + 1;
} else {
// 如果最后一位不相等,则对称判断前面的取最大值
dp[row][j] = Math.max(dp[prevRow][j], dp[row][j - 1]);
}
}
}

3. 优化成一维数组

每行的值都是从左向右赋值的,可以将二维优化成一维数组
for (int row = 1; row < rows; row++) {
for (int col = 0; col < cols; col++) {
if (col == 0) {
dp[0] = 0;
int j = row;
while (j >= 0) {
dp[col] += grid[j][0];
j--;
}
} else {
dp[col] = Math.max(dp[col], dp[col - 1]) + grid[row][col];
}
}
}

排序

各种排序时间复杂度

在这里插入图片描述

1. 分割区间最好左闭右开[begin, end)

有利于计算区间长度`length = end - begin;`,保证了mid的取值。
例如归并排序中:
int mid = (begin + end) >> 1;
// [begin,mid)
sort(begin,mid);
// [mid,end)
sort(mid,end);

merge(begin,mid,end);

2. 冒泡排序和插入排序优化

冒泡排序:加入endIndex记录**最后一次交换位置(后面的就不用去冒泡了)**,初始值为完全有序时的下标为,即1
int endIndex = 1;
for (int begin = 1; begin <= end; begin++) {
if (cmp(begin,begin - 1) < 0) {
swap(begin,begin - 1);
// 记录交换位置
endIndex = begin;
}
end = endIndex;
}
插入排序(打扑克排序):时间复杂度与**逆序对**个数成正比

优化:
  1. (重点)先将待插入的数据备份,挪动前面的元素后,再插入
  2. (进阶)利用二分搜索,优化了比较的次数,但是挪动没变,时间复杂度没变
for (int begin = 1; begin < array.length; begin++) {
/*----------------------------------------------------*/
int cur = begin;
// 数据备份
E element = array[begin];
while (cur > 0 && cmp(cur, cur - 1) < 0) {
// 1.交换
swap(cur, cur - 1);

// 2.将交换改为挪动
array[cur] = array[cur - 1];
// cur减一之后为begin所在的值,依次比较然后插入
cur--;

}
array[cur] = element;
/*----------------------------------------------------*/
// 3.采用二分搜索进行插入优化
// 数据备份
E element = array[begin];
int insertIndex = search(begin);
for (int i = begin - 1; i >= insertIndex; i--) {
array[i + 1] = array[i];
}
array[insertIndex] = element;
}

3. 归并排序合并时避免,讨论右结束时左全挪动情况

即`ri = re`时,仍然是左边数组进行搬运,如代码 else块 所示,`ri = re`时`array[ai] = leftArray[li];`
// li le 左新建序列起始,ri re 右序列起始,ai为遍历数组指针
for (int i = li; i < le; i++) {
// 重点:左边序列起始是begin + i
leftArray[i] = array[begin + i];
}

// 左边还没结束
while (li < le) {
// 保证稳定性,且ri = re了,即左边全部挪过去
if (ri < re && cmp(leftArray[li], array[ri]) > 0) {
array[ai] = array[ri];
ai++;
ri++;

} else {
array[ai] = leftArray[li];
ai++;
li++;
}
}

4. 快排轴点随机设置,优化算法避免两边不均匀

// 避免出现最坏情况进行优化方案:
// 随机选取一个值与begin位置替换,作为轴点
int length = end - begin;
swap(begin,begin + (int) (Math.random() * length));

E pivot = array[begin];

数学

1. 快速幂解决Pow(x, n)

3^21 = 3^(1*2^0 + 0*2^1 + 1*2^2 + 0*2^3 + 1*2^4)

= 3^2^0 * 3^2^2 * 3^2^4

/**
* 快速幂解决
* 21 = 10101(二进制)
* 3^21 = 3^(1*2^0 + 0*2^1 + 1*2^2 + 0*2^3 + 1*2^4)
* 如果指数是负数,则最后分之1即可,1/3^21
* @param x
* @param n
* @return
*/
public double myPow1(double x, int n) {
double res = 1.0;
// 考虑边界条件:n 是 32 位有符号整数,其数值范围是 [?231, 231 ? 1] 。
long y = n;
if (n < 0) {
y = -((long)n);
}

while (y > 0) {
// 取n的最后一个二进制位,看乘不乘
if ((y & 1) == 1) {
res *= x;
}

// 下一次的底数
x = x * x;

// 舍弃到最后一位
y = y >> 1;
}
return (n < 0) ? (1 / res) : res;
}

2. 快速幂补充 x^y%z

​ 可能x、y都是很大的整数,导致乘法就会溢出,则(a * b) % p = ((a % p) * (b % p)) % p

3. 约瑟夫环公式

公式推导:f(n, m) = (f(n - m, m) + m) % n

img

哈希

​ 哈希表,不考虑顺序、不考虑 Key 的可比较性(指的是不需要元素之间有大小顺序,而不是不能判断相等与否)

数组实现,典型的空间换时间,HashMap底层的hash函数。

在这里插入图片描述

联想关键词

  • 搜索数据要求O(1)的时间

1. LRU:哈希表 + 双向链表

​ 这是一种经典的数据结构,在jdk1.8之前,Java中的HashMap采取这种数据结构存储

LRU:最近最少使用,这种页面置换算法,我们可以采取哈希表 + 双向链表实现,在Java中有LinkedHashMap,就是这种结构。

  • 哈希表(存储数据):因为缓存中存储的不是个值,键值对key-value,所以需要二维的存储
  • 双向链表(存储顺序):使用双向链表的目的是(1)能够在头部插入节点 (2)删除能删除尾结点
/**
* 获取key对应的值
* @param key
* @return
*/
public int get(int key) {
Node node = map.get(key);
if (node == null) {
return -1;
}

// 有的话,就将其插到头部去,因为最新操作过
removeNode(node);
addAfterFirst(node);

return node.value;
}

/**
* 从链表中删除
* @param node
*/
private void removeNode(Node node) {
node.prev.next = node.next;
node.next.prev = node.prev;
}

/**
* 加在first的后面
* @param node
*/
private void addAfterFirst(Node node) {
node.next = first.next;
first.next.prev = node;

first.next = node;
node.prev = first;
}

/**
* put进去一个值
* 1.无该值,则put进去且更新成最新
* 2.有该值,更新成最新
* @param key
* @param value
*/
public void put(int key, int value) {
Node node = map.get(key);
if (node != null) {
node.value = value;
// 更新完后,也要将其插到头部去,因为最新操作过
removeNode(node);
addAfterFirst(node);
} else {
if (map.size() == capacity) {
// 淘汰掉尾部节点
Node remove = map.remove(last.prev.key);
removeNode(remove);
}

map.put(key, node = new Node(key, value));
// 就将其插到头部去,因为最新操作过
addAfterFirst(node);
}
}

​ 堆,操作一个排序好的数据,例如最大值,最小值,O(1)级别,用数组存放,用二叉搜索树则杀鸡用牛刀。

1. 优先队列

​ 优先队列,每次拿取一组数据的最值(例如:最小的,最大的),所以就可以采用大顶堆、小顶堆来实现。

Trie

​ 字典树,对一堆不重复字符串进行前缀搜索,效率只跟字符串长度有关

并查集

1. 尽量使用Quick Union基于路径分裂/路径减半 + 基于rank优化或者size优化

​ 实质上:基于路径分裂/路径减半优化find、基于rank优化或者size优化union

(1)不使用路径压缩是因为虽然树高度降低离谱,但是成本很高,每个节点都要压缩

/*路径分裂:使每个节点指向祖父即可*/
if (v != parents[v]) {
int parent = parents[v];
parents[v] = parents[parent];
v = parent;
}
return v;
/*路径减半:跳一个节点指向祖父节点,中间的依然只指向父亲不动*/
if (v != parents[v]) {
parents[v] = parents[parents[v]];
v = parents[v];
}
return v;

(2)基于rank的优化更好一点,避免数过高

/*1. 基于size优化,size小的跟随size大的,降低树高度---------*/
if (sizes[p1] < sizes[p2]) {
// v2的根结点赋值给v1的根结点
parents[p1] = p2;
sizes[p2] += sizes[p1];
} else {
// v1的根结点赋值给v2的根结点
parents[p2] = p1;
sizes[p1] += sizes[p2];
}

/*2. 基于rank的优化(树的高度),rank小的跟随rank大的,降低树高度*/
if (ranks[p1] < ranks[p2]) {
// v2的根结点赋值给v1的根结点
parents[p1] = p2;
} else if (ranks[p1] > ranks[p2]){
// v1的根结点赋值给v2的根结点
parents[p2] = p1;
} else {
// 随便选个,然后被指向的要加一层高度
parents[p1] = p2;
ranks[p2]++;
}

贪心

分治

1. Pow(x, n)

​ 使用递归分治解决:x^n = x^(n/2) * x^(n/2)

陷阱:n可能为负数,负数分治到最后会到-1,而-1 >> 1还是-1,则-1需要直接返回

/**
* 使用递归分治解决
* 陷阱:n可能为负数,负数分治到最后会到-1,而-1 >> 1还是-1,则-1需要直接返回
* @param x
* @param n
* @return
*/
public double myPow(double x, int n) {
if (n == 0) {
return 1;
}

// n可能为负数,则-1需要直接返回
if (n == -1) {
return 1 / x;
}

// 判断指数是否为奇数
boolean flag = (n & 1) == 1;

double half = myPow(x, n >> 1);
return flag ? half * half * x : half * half;
}