算法题
题目来源于LeedCode热题100,取其中个人觉得较难的部分。
双指针
接雨水
正向遍历+反向遍历+取最小值
java
public int trap(int[] height) {
int left=0;
int right=height.length-1;
int water=0;
int leftMax = 0, rightMax = 0;
while(left<right){
leftMax = Math.max(leftMax, height[left]);
rightMax = Math.max(rightMax, height[right]);
if(height[left]<height[right]){
water += leftMax - height[left];
left++;
}
else{
water += rightMax - height[right];
right--;
}
}
return water;
}
数组
最大子数组和
迭代计算以当前元素结尾的子数组的最大和(动态规划可秒)
java
public int maxSubArray(int[] nums) {
int pre = 0, maxAns = nums[0];
for (int x : nums) {
pre = Math.max(pre + x, x);//以当前元素结尾的子数组的最大和
maxAns = Math.max(maxAns, pre);
}
return maxAns;
}
缺失的第一个正数
1.所有负数变为数组长度+1,排除在外
2.将元素<数组长度的对应位置变为负数
3.返回第一个大于0的数组下标
java
public int firstMissingPositive(int[] nums) {
int n = nums.length;
for (int i = 0; i < n; ++i) {//负数变正数
if (nums[i] <= 0) {
nums[i] = n + 1;
}
}
for (int i = 0; i < n; ++i) {
int num = Math.abs(nums[i]);
if (num <= n) {
nums[num - 1] = -Math.abs(nums[num - 1]);//对应位置变负数
}
}
for (int i = 0; i < n; ++i) {
if (nums[i] > 0) {
return i + 1;
}
}
return n + 1;
}
矩阵
螺旋遍历矩阵
用矩阵存储走过的区域,二维数组存储方向
java
public List<Integer> spiralOrder(int[][] matrix) {
List<Integer> order = new ArrayList<Integer>();
if (matrix == null || matrix.length == 0 || matrix[0].length == 0) {
return order;
}
int rows = matrix.length, columns = matrix[0].length;
boolean[][] visited=new boolean[rows][columns];
int total = rows * columns;
int row = 0, column = 0;
int[][] directions = {{0, 1}, {1, 0}, {0, -1}, {-1, 0}};
int directionIndex = 0;
for (int i = 0; i < total; i++) {
order.add(matrix[row][column]);
visited[row][column] = true;
int nextRow = row + directions[directionIndex][0], nextColumn = column + directions[directionIndex][1];
if (nextRow < 0 || nextRow >= rows || nextColumn < 0 || nextColumn >= columns || visited[nextRow][nextColumn]) {
directionIndex = (directionIndex + 1) % 4;
}
row += directions[directionIndex][0];
column += directions[directionIndex][1];
}
return order;
}
链表
K个一组翻转链表
迭代的进行翻转,每次翻转K个
java
public ListNode reverseKGroup(ListNode head, int k) {
ListNode hair = new ListNode(0);
hair.next = head;
ListNode pre = hair;
while (head != null) {
ListNode tail = pre;
// 查看剩余部分长度是否大于等于 k
for (int i = 0; i < k; ++i) {
tail = tail.next;
if (tail == null) {
return hair.next;
}
}
ListNode nex = tail.next;
ListNode[] reverse = myReverse(head, tail);
head = reverse[0];
tail = reverse[1];
// 把子链表重新接回原链表
pre.next = head;
tail.next = nex;
pre = tail;
head = tail.next;
}
return hair.next;
}
public ListNode[] myReverse(ListNode head, ListNode tail) {
ListNode prev = tail.next;
ListNode p = head;
while (prev != tail) {
ListNode nex = p.next;
p.next = prev;
prev = p;
p = nex;
}
return new ListNode[]{tail, head};
}
深拷贝
递归拷贝next节点和random节点,并用哈希表保证不拷贝重复节点
java
Map<Node, Node> cachedNode = new HashMap<Node, Node>();
public Node copyRandomList(Node head) {
if (head==null){
return null;
}
if (!cachedNode.containsKey(head)) {
Node headNew = new Node(head.val);
cachedNode.put(head, headNew);
headNew.next = copyRandomList(head.next);
headNew.random = copyRandomList(head.random);
}
return cachedNode.get(head);
}
归并排序链表
1.找到链表的中点,以中点为分界,将链表拆分成两个子链表(快慢指针)
2.对两个子链表分别排序
3.将两个排序后的子链表合并,得到完整的排序后的链表
4.进行递归,递归的终止条件是链表的节点个数小于或等于 1
java
public ListNode sortList(ListNode head) {
return sortList(head, null);
}
public ListNode sortList(ListNode head, ListNode tail){
if (head == null) {
return head;
}
if (head.next == tail) {
head.next = null;
return head;
}
ListNode slow=head,fast=head;
while(fast!=tail){
slow=slow.next;
fast=fast.next;
if(fast!=tail){
fast=fast.next;
}
}//慢指针在中心
ListNode mid=slow;
ListNode list1 = sortList(head, mid);
ListNode list2 = sortList(mid, tail);
ListNode sorted = merge(list1, list2);
return sorted;
}
//合并两个有序链表见21
public ListNode merge(ListNode list1, ListNode list2){
ListNode sum;
ListNode head;
if(list1==null){
return list2;
}
if(list2==null){
return list1;
}
if(list1.val>list2.val){
sum=list2;
head=list2;
list2=list2.next;
}
else{
sum=list1;
head=list1;
list1=list1.next;
}
while(list1!=null&&list2!=null){
if(list1.val>list2.val){
sum.next=list2;
list2=list2.next;
sum=sum.next;
}
else{
sum.next=list1;
list1=list1.next;
sum=sum.next;
}
}
if(list1==null&&list2!=null){
sum.next=list2;
}
if(list2==null&&list1!=null){
sum.next=list1;
}
return head;
}
归并、快速排序数组
LRU缓存
双向链表存取节点数据,哈希表快速查找指定元素
java
class DLinkedNode {
int key;//方便哈希表寻找
int value;
DLinkedNode prev;
DLinkedNode next;
public DLinkedNode() {}
public DLinkedNode(int _key, int _value) {key = _key; value = _value;}
}
private Map<Integer, DLinkedNode> cache = new HashMap<Integer, DLinkedNode>();
private int size;
private int capacity;
private DLinkedNode head, tail;//虚拟节点,不存储数据
public LRUCache(int capacity) {//初始化存储容量
this.size = 0;
this.capacity = capacity;
// 使用伪头部和伪尾部节点
head = new DLinkedNode();
tail = new DLinkedNode();
head.next = tail;
tail.prev = head;
}
public int get(int key) {
DLinkedNode node = cache.get(key);
if (node == null) {
return -1;
}
// 如果 key 存在,先通过哈希表定位,再移到头部
moveToHead(node);
return node.value;
}
public void put(int key, int value) {
DLinkedNode node = cache.get(key);
if (node == null) {
// 如果 key 不存在,创建一个新的节点
DLinkedNode newNode = new DLinkedNode(key, value);
// 添加进哈希表
cache.put(key, newNode);
// 添加至双向链表的头部
addToHead(newNode);
++size;
if (size > capacity) {
// 如果超出容量,删除双向链表的尾部节点
DLinkedNode tail = removeTail();
// 删除哈希表中对应的项
cache.remove(tail.key);
--size;
}
}
else {
// 如果 key 存在,先通过哈希表定位,再修改 value,并移到头部
node.value = value;
moveToHead(node);
}
}
private void addToHead(DLinkedNode node) {
node.prev = head;
node.next = head.next;
head.next.prev = node;
head.next = node;
}
private void removeNode(DLinkedNode node) {
node.prev.next = node.next;
node.next.prev = node.prev;
}
private void moveToHead(DLinkedNode node) {
removeNode(node);
addToHead(node);
}
private DLinkedNode removeTail() {
DLinkedNode res = tail.prev;
removeNode(res);
return res;
}
二叉树
层序遍历
利用队列先进先出取根节点存子节点
java
public List<List<Integer>> levelOrder(TreeNode root) {
List<List<Integer>> ret = new ArrayList<List<Integer>>();
if (root == null) {
return ret;
}
Queue<TreeNode> queue = new LinkedList<TreeNode>();
queue.offer(root);
while (!queue.isEmpty()) {
List<Integer> level = new ArrayList<Integer>();
int currentLevelSize = queue.size();
for (int i = 1; i <= currentLevelSize; ++i) {
TreeNode node = queue.poll();
level.add(node.val);
if (node.left != null) {
queue.offer(node.left);
}
if (node.right != null) {
queue.offer(node.right);
}
}
ret.add(level);
}
return ret;
}
前序+中序遍历构造二叉树
拆分为左子树和右子树再进行递归
java
public TreeNode buildTree(int[] preorder, int[] inorder) {
if(preorder==null||inorder==null||preorder.length == 0||inorder.length == 0){
return null;
}
if(preorder.length==1){
return new TreeNode(preorder[0]);
}
TreeNode root=new TreeNode(preorder[0]);
int index = -1;
for(int i=0;i<preorder.length;i++){
if(inorder[i]==preorder[0]){
index=i;
break;
}
}
int[] leftIno = new int[index]; // 创建新数组
int[] rightIno=new int[preorder.length-index-1];
int[] leftPre=new int[index];
int[] rightPre=new int[preorder.length-index-1];
System.arraycopy(inorder, 0, leftIno, 0, index);
System.arraycopy(inorder, index+1, rightIno, 0, preorder.length-index-1);
System.arraycopy(preorder, 1, leftPre, 0, index);
System.arraycopy(preorder, index+1, rightPre, 0, preorder.length-index-1);
root.left=buildTree(leftPre,leftIno);
root.right=buildTree(rightPre,rightIno);
return root;
}
图论
课程表
这个学期必须选修 numCourses 门课程,记为 0 到 numCourses - 1 。
在选修某些课程之前需要一些先修课程。 先修课程按数组 prerequisites 给出,其中 prerequisites[i] = [ai, bi] ,表示如果要学习课程 ai 则 必须 先学习课程 bi 。
用二维数组存储邻接表,再按深度优先遍历节点,每种节点有3种状态。
java
class Solution {
List<List<Integer>> edges;
int[] visited;
boolean valid = true;
public boolean canFinish(int numCourses, int[][] prerequisites) {
//初始化
edges = new ArrayList<List<Integer>>();
for (int i = 0; i < numCourses; ++i) {
edges.add(new ArrayList<Integer>());
}
visited = new int[numCourses];
for (int[] info : prerequisites) {
//edges.get(0) 返回一个列表,表示从节点 0 出发的所有邻接节点
//存储所有边信息
//将目标节点 info[0] 添加到源节点 info[1] 的邻接表中
edges.get(info[1]).add(info[0]);
}
for (int i = 0; i < numCourses && valid; ++i) {
if (visited[i] == 0) {
dfs(i);
}
}
return valid;
}
//visited[u]=0未开始;=1进行中;=2已结束。如果找到进行中的节点,说明有环
public void dfs(int u) {
visited[u] = 1;
for (int v: edges.get(u)) {
if (visited[v] == 0) {
dfs(v);
if (!valid) {
return;
}
} else if (visited[v] == 1) {
valid = false;
return;
}
}
visited[u] = 2;
}
}
前缀数Trie(字典树)
树形结构,每个父节点有26个子节点,用于快速查找某个字符串是否存在
java
class Trie {
private Trie[] children;
private boolean isEnd;
public Trie() {
children = new Trie[26];
isEnd = false;
}
//插入单词
public void insert(String word) {
Trie node = this;
for (int i = 0; i < word.length(); i++) {
char ch = word.charAt(i);
int index = ch - 'a';
if (node.children[index] == null) {
node.children[index] = new Trie();
}
node = node.children[index];
}
node.isEnd = true;
}
//搜索单词
public boolean search(String word) {
Trie node = searchPrefix(word);
return node != null && node.isEnd;
}
//搜索单词前缀
public boolean startsWith(String prefix) {
return searchPrefix(prefix) != null;
}
private Trie searchPrefix(String prefix) {
Trie node = this;
for (int i = 0; i < prefix.length(); i++) {
char ch = prefix.charAt(i);
int index = ch - 'a';
if (node.children[index] == null) {
return null;
}
node = node.children[index];
}
return node;
}
}
栈
单调栈使用场景:在一维数组中找第一个满足某种条件(更大/更小)的数
单调栈
设计一个支持 push ,pop ,top 操作,并能在常数时间内检索到最小元素的栈
使用双栈 或二维数组分别存储元素的值和该元素及里面其它元素的最小值
柱状图中最大的矩形(单调栈应用)
目标是找到左右两侧最近的高度小于 h 的柱子,维护一个存储下标的栈,栈中存放的下标对应的值单调递增,若遍历到的数小于栈顶的值,则将小于的值全部移除。
堆
核心是上浮和下沉
前K个高频元素
堆与Map与自定义比较器的结合
回溯
全排列
给定一个不含重复数字的数组 nums ,返回其所有可能的全排列。
核心在于每次选一个未选过的数放入output数组,数组满了则存储结果
可以用n分割当前未放入的数与已经放入的数
时间复杂度为O(n*n!)
分割回文串
将字符串 s 分割成一些子串,使每个子串都是回文串 ,返回 s 所有可能的分割方案
动态规划预处理所有是回文串的子串,再回溯
二分算法
主要在判断边界条件的地方比较麻烦
搜索旋转排序数组
nums 在预先未知的某个下标 k(0 <= k < nums.length)上进行了旋转,例如 [0,1,2,4,5,6,7] 在下标 3 处经旋转后可能变为 [4,5,6,7,0,1,2] 。
如果 nums 中存在这个目标值 target ,则返回它的下标,否则返回 -1 。
仍然能进行二分查找
cpp
class Solution {
public:
int search(vector<int>& nums, int target) {
int n = (int)nums.size();
if (!n) {
return -1;
}
if (n == 1) {
return nums[0] == target ? 0 : -1;
}
int l = 0, r = n - 1;
while (l <= r) {
int mid = (l + r) / 2;
if (nums[mid] == target) return mid;
if (nums[0] <= nums[mid]) {
if (nums[0] <= target && target < nums[mid]) {
r = mid - 1;
} else {
l = mid + 1;
}
} else {
if (nums[mid] < target && target <= nums[n - 1]) {
l = mid + 1;
} else {
r = mid - 1;
}
}
}
return -1;
}
};
寻找两个有序数组的中位数
给定两个大小分别为 m 和 n 的正序(从小到大)数组 nums1 和 nums2,找出并返回这两个正序数组的中位数
等效于找两个数组的第 k=(n+m)/2 个数
可以比较 num1[k/2−1] 和 num2[k/2−1],如果num1[k/2-1]较小,则可以排除 num1[0] 到 num1[k/2−1]的数,然后更新k,继续进行比较直到找到目标值。
时间复杂度O(log(n+m))
cpp
public:
double findMedianSortedArrays(vector<int>& nums1, vector<int>& nums2) {
int l=nums1.size();int r=nums2.size();
int count=l+r;
if(count%2==1){
return getKthElement(nums1,nums2,(count+1)/2);
}else{
return (getKthElement(nums1,nums2,count/2)+getKthElement(nums1, nums2,count/2+1))/2.0;
}
}
int getKthElement(const vector<int>& nums1, const vector<int>& nums2, int k) {
int m = nums1.size();
int n = nums2.size();
int index1 = 0, index2 = 0;
while(true){
if(index1==m){
return nums2[index2+k-1];
}
if(index2==n){
return nums1[index1+k-1];
}
if(k==1){
return min(nums1[index1],nums2[index2]);
}
int newIndex1 = min(index1 + k / 2 - 1, m - 1);
int newIndex2 = min(index2 + k / 2 - 1, n - 1);
int pivot1 = nums1[newIndex1];
int pivot2 = nums2[newIndex2];
if (pivot1 <= pivot2) {
k -= newIndex1 - index1 + 1;
index1 = newIndex1 + 1;
}
else {
k -= newIndex2 - index2 + 1;
index2 = newIndex2 + 1;
}
}
}
};
贪心算法
跳跃游戏
每个元素 nums[i] 表示从索引 i 向后跳转的最大长度。如果在 nums[i] 处,可以跳转到任意 nums[i + j] 处,返回到达 nums[n - 1] 的最小跳跃次数。
java
class Solution {
public int jump(int[] nums) {
int position = nums.length - 1;
int steps = 0;
while (position > 0) {
for (int i = 0; i < position; i++) {
if (i + nums[i] >= position) {
position = i;
steps++;
break;
}
}
}
return steps;
}
}
动态规划
最长有效括号(时间复杂度O(n))
如果找到每个可能的子串后判断它的有效性,时间复杂度会达到 O ( n 3 ) \ O(n^3) O(n3)
用动态规划就只有 O ( n ) \ O(n) O(n)
java
class Solution {
public int longestValidParentheses(String s) {
int maxans = 0;
int[] dp = new int[s.length()];
for (int i = 1; i < s.length(); i++) {
if (s.charAt(i) == ')') {
//情况1: s[i]=')' 且 s[i−1]='('
if (s.charAt(i - 1) == '(') {
dp[i] = (i >= 2 ? dp[i - 2] : 0) + 2;
}
//情况2: s[i]=')' 且 s[i−1]=')',需要继续判断 s[i−dp[i−1]−1]='('
else if (i - dp[i - 1] > 0 && s.charAt(i - dp[i - 1] - 1) == '(') {
dp[i] = dp[i - 1] + ((i - dp[i - 1]) >= 2 ? dp[i - dp[i - 1] - 2] : 0) + 2;
}
maxans = Math.max(maxans, dp[i]);
}
}
return maxans;
}
}
单词拆分(别用回溯太慢了)
一个只包含正整数的非空数组 nums ,判断是否可以将这个数组分割成两个子集,使得两个子集的元素和相等
dp[i][j] 表示从数组的 [0,i] 下标范围内选取若干个正整数(可以是 0 个),是否存在一种选取方案使得被选取的正整数的和等于 j
cpp
class Solution {
public boolean canPartition(int[] nums) {
int n = nums.length;
if (n < 2) {
return false;
}
int sum = 0, maxNum = 0;
for (int num : nums) {
sum += num;
maxNum = Math.max(maxNum, num);
}
if (sum % 2 != 0) {
return false;
}
int target = sum / 2;
if (maxNum > target) {
return false;
}
boolean[][] dp = new boolean[n][target + 1];
for (int i = 0; i < n; i++) {
dp[i][0] = true;
}
dp[0][nums[0]] = true;
for (int i = 1; i < n; i++) {
int num = nums[i];
for (int j = 1; j <= target; j++) {
if (j >= num) {
dp[i][j] = dp[i - 1][j] | dp[i - 1][j - num];
} else {
dp[i][j] = dp[i - 1][j];
}
}
}
return dp[n - 1][target];
}
}
编辑距离
两个单词 word1 和 word2, 返回将 word1 转换成 word2 所使用的最少操作数
可以对一个单词进行三种操作:插入一个字符;删除一个字符;替换一个字符
用 dp[i][j] 表示 A 的前 i 个字母和 B 的前 j 个字母之间的编辑距离
java
public int minDistance(String word1, String word2) {
int c1=word1.length();
int c2=word2.length();
int[][] dp=new int[c1+1][c2+1];
for(int i=0;i<=c1;++i){
dp[i][0]=i;
}
for(int j=0;j<=c2;++j){
dp[0][j]=j;
}
for(int i=1;i<=c1;++i){
for(int j=1;j<=c2;++j){
//取1.在a中插入一个单词 2.在b中插入一个单词 3.在a中替换一个单词 操作中的最小值
if(word1.charAt(i-1)!=word2.charAt(j-1)){
dp[i][j]=1+Math.min(Math.min(dp[i][j-1],dp[i-1][j]),dp[i-1][j-1]);
}else{
dp[i][j]=1+Math.min(Math.min(dp[i][j-1],dp[i-1][j]),dp[i-1][j-1]-1);
}
}
}
return dp[c1][c2];
}
最长回文子串
可用一维/二维动态规划做,这里是另一种方法
枚举所有的「回文中心」并尝试「扩展」,直到无法扩展为止
java
class Solution {
public String longestPalindrome(String s) {
if (s == null || s.length() < 1) {
return "";
}
int start = 0, end = 0;
for (int i = 0; i < s.length(); i++) {
int len1 = expandAroundCenter(s, i, i);
int len2 = expandAroundCenter(s, i, i + 1);
int len = Math.max(len1, len2);
if (len > end - start) {
start = i - (len - 1) / 2;
end = i + len / 2;
}
}
return s.substring(start, end + 1);
}
public int expandAroundCenter(String s, int left, int right) {
while (left >= 0 && right < s.length() && s.charAt(left) == s.charAt(right)) {
--left;
++right;
}
return right - left - 1;
}
}
技巧
找唯一一个只出现一次的数
全部的数进行异或运算,相同的数异或后为0抵消,最后留下唯一只出现一次的数
java
for (int num : nums) {
single ^= num;
}
找出现次数大于一半的数
如果一个数组有大于一半的数相同,那么任意删去两个不同的数字,新数组还是会有相同的性质
java
int winner = nums[0];
int count = 1;
for (int i = 1; i < nums.length; i++) {
if (winner == nums[i]) {
count++;
} else if (count == 0) {
winner = nums[i];
count++;
} else {
count--;
}
}
return winner;
寻找重复数
数字都在 [1, n] 范围内,重复数一定在数值和索引之间成环且在环的入口,然后通过快慢指针找到重复数
java
public int findDuplicate(int[] nums) {
int slow = 0, fast = 0;
do {
slow = nums[slow];
fast = nums[nums[fast]];
} while (slow != fast);
//找到环入口
slow = 0;
while (slow != fast) {
slow = nums[slow];
fast = nums[fast];
}
return slow;
}
快速求x的y次方
常规时间复杂度是O(y),使用快速幂算法的时间复杂度为O(logy),关键在于将y转换为二进制数组再进行计算
例:x=5, y=13, 即y=1101,遍历每一位,计算当前底数x *= x,并判断指数,为1则结果乘上当前底数,为0则不用乘
java
public static double power(double x, int y) {
double result = 1.0; // 初始化结果为1
long exp = Math.abs((long)y); // 将指数y转为正数(避免负数问题)
while (exp > 0) {
// 如果当前指数是奇数,将当前底数乘入结果
if ((exp & 1) == 1) {
result *= x;
}
// 平方当前底数
x *= x;
// 指数右移(等价于除以2)
exp >>= 1;
}
// 如果原指数为负数,返回1/result
return y < 0 ? 1 / result : result;
}