贪心算法通常适用于一些局部最优解即为全局最优解的问题,简单高效;而动态规划适用于需要求解全局最优解的问题,能够通过存储子问题解来避免重复计算。
62.实现前缀树
关键算法:
算法思想:套娃。每一个Trie类型中有一个具有26个Trie类型的数组,这样只要插入的字符出现过,就能在字典中找到它的前缀。使用 isEnd来表示字符是不是结束。
关键算法:
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) { // 只保存所有插入的字符中第i位没有过的字符,如只有acx和abx,则第一个数组中只有a,第二个数组中有b,c,第三个数组中只有x。将所有插入的字符都作为了前缀的字典。
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;
}
}
63.替换单词
关键算法:
算法思想:哈希集合。将字典中的字符串保存在set中,然后一次遍历每个词,并取每个前缀,如果字典中包含了前缀,则将这个单词替换。
关键算法:
class Solution {
public String replaceWords(List<String> dictionary, String sentence) {
Set<String> dictionarySet = new HashSet<String>();
for (String root : dictionary) {
dictionarySet.add(root);
}
String[] words = sentence.split(" ");
for (int i = 0; i < words.length; i++) {
String word = words[i];
for (int j = 0; j < word.length(); j++) {
if (dictionarySet.contains(word.substring(0, 1 + j))) {
words[i] = word.substring(0, 1 + j);
break;
}
}
}
return String.join(" ", words);
}
}
方法二:
关键算法:
算法思想:字典树。
关键算法:
class Solution {
public String replaceWords(List<String> dictionary, String sentence) {
Trie trie = new Trie();
for (String word : dictionary) {
Trie cur = trie;
for (int i = 0; i < word.length(); i++) { // 将dic中的字符串放如字典中
char c = word.charAt(i);
cur.children.putIfAbsent(c, new Trie());
cur = cur.children.get(c);
}
cur.children.put('#', new Trie()); // 结束字符
}
String[] words = sentence.split(" ");
for (int i = 0; i < words.length; i++) { // 对每个字进行检测前缀。
words[i] = findRoot(words[i], trie);
}
return String.join(" ", words);
}
public String findRoot(String word, Trie trie) { // trie为根
StringBuffer root = new StringBuffer();
Trie cur = trie;
for (int i = 0; i < word.length(); i++) {
char c = word.charAt(i);
if (cur.children.containsKey('#')) { // 它不是结束字符,则返回保存的前缀
return root.toString();
}
if (!cur.children.containsKey(c)) { // 不包含这个字符,说明前缀不在,返回这个单词
return word;
}
root.append(c); // 包含这个单词,将单词保存,以便返回。
cur = cur.children.get(c);
}
return root.toString();
}
}
class Trie {
Map<Character, Trie> children;
public Trie() {
children = new HashMap<Character, Trie>();
}
}
64.神奇的字典
关键算法:
算法思想:枚举每个字典中的字符串并判断。对搜索的单词进行和字典中的每个词进行判断,如果只有一个字符不同,那么返回true,如果大于一个,则跳过,进入字典中下一个。
关键算法:
class MagicDictionary {
private String[] words;
public MagicDictionary() {
}
public void buildDict(String[] dictionary) {
words = dictionary;
}
public boolean search(String searchWord) {
for (String word : words) {
if (word.length() != searchWord.length()) {
continue;
}
int diff = 0;
for (int i = 0; i < word.length(); ++i) {
if (word.charAt(i) != searchWord.charAt(i)) {
++diff;
if (diff > 1) {
break;
}
}
}
if (diff == 1) {
return true;
}
}
return false;
}
}
65.最短的单词编码
关键算法:
算法思想:存储后缀。
关键算法:
class Solution {
public int minimumLengthEncoding(String[] words) {
Set<String> good = new HashSet<String>(Arrays.asList(words));
for (String word: words) {
for (int k = 1; k < word.length(); ++k) {
good.remove(word.substring(k)); // 如果这个单词出现在这个单词的字串里,则将它从set中删除。
}
}
int ans = 0;
for (String word: good) { // 遍历set,取出每个单词,并计算它的长度+1。
ans += word.length() + 1;
}
return ans;
}
}
66.单词之和
关键算法:
算法思想:暴力扫描。
关键算法:
class MapSum {
Map<String, Integer> map;
public MapSum() {
map = new HashMap<>();
}
public void insert(String key, int val) {
map.put(key,val);
}
public int sum(String prefix) {
int res = 0;
for (String s : map.keySet()) {
if (s.startsWith(prefix)) {
res += map.get(s);
}
}
return res;
}
}
67.最大的异或
关键算法:
算法思想:哈希表。
关键算法:
class Solution {
// 最高位的二进制位编号为 30
static final int HIGH_BIT = 30;
public int findMaximumXOR(int[] nums) {
int x = 0;
for (int k = HIGH_BIT; k >= 0; --k) {
Set<Integer> seen = new HashSet<Integer>();
// 将所有的 pre^k(a_j) 放入哈希表中
for (int num : nums) {
// 如果只想保留从最高位开始到第 k 个二进制位为止的部分
// 只需将其右移 k 位
seen.add(num >> k);
}
// 目前 x 包含从最高位开始到第 k+1 个二进制位为止的部分
// 我们将 x 的第 k 个二进制位置为 1,即为 x = x*2+1
int xNext = x * 2 + 1;
boolean found = false;
// 枚举 i
for (int num : nums) {
if (seen.contains(xNext ^ (num >> k))) {
found = true;
break;
}
}
if (found) {
x = xNext;
} else {
// 如果没有找到满足等式的 a_i 和 a_j,那么 x 的第 k 个二进制位只能为 0
// 即为 x = x*2
x = xNext - 1;
}
}
return x;
}
}
68.查找插入位置
关键算法:
算法思想:二分查找。在一个有序的数组中,通过缩小左右两边索引,将位置确定。
关键算法:
class Solution {
public int searchInsert(int[] nums, int target) {
int n = nums.length;
int left = 0, right = n - 1, ans = n;
while (left <= right) { // 找到第一个小于等于目标值的值
int mid = ((right - left) >> 1) + left;
if (target <= nums[mid]) {
ans = mid;
right = mid - 1;
} else {
left = mid + 1;
}
}
return ans;
}
}
关键算法:
算法思想:二分查找。在一个数组中,数组中的某个值大于左边的值,小于右边的值。则返回该值。
关键算法:
class Solution {
public int peakIndexInMountainArray(int[] arr) {
int n = arr.length;
int left = 1, right = n - 2, ans = 0;
while (left <= right) {
int mid = (left + right) / 2;
if (arr[mid] > arr[mid + 1]) { // 如果arr[mid] > arr[mid + 1],则该值所在区间在mid-1左边,或者就是mid。
ans = mid;
right = mid - 1;
} else { //如果arr[mid] <= arr[mid + 1],则该值所在区间在mid+1后面,不包括mid
left = mid + 1;
}
}
return ans;
}
}
70.排序数组中只出现一次的数字
关键算法:
算法思想:二分查找。在一个有序数组中,找到只出现一次的值x的索引,其他值出现两次,这个出现一次的值,左边是偶数个,右边是偶数个,但是左边是偶数m:arr[m]=arr[m+1],右边是奇数m:arr[m]=arr[m+1]。所以,当arr[m]=arr[m+1](此时m为偶数),或者arr[m]=arr[m-1](此时m为奇数)时,说明此时的m在x的左边。当arr[m]=arr[m+1](此时m为奇数),或者arr[m]=arr[m-1](此时m为偶数)时,说明此时的m在x的右边。如果使用异或来处理,那么判断在左边可以用方法mid^1的方法,因为偶数与1异或是mid+1。奇数与1异或是mid-1。所以可以用这中方式一次性判断是否在左边,否则在右边。形成二分查找的方式。
关键算法:
class Solution {
public int singleNonDuplicate(int[] nums) {
int low = 0, high = nums.length - 1;
while (low < high) {
int mid = (high - low) / 2 + low;
if (nums[mid] == nums[mid ^ 1]) { // 因为判断条件中,mid要+1,所以low一定要小于high,因为如果等于high,+1后会超出范围。当low<high时,符合要求的那一边可以不用对1进行操作.
low = mid + 1;
} else {
high = mid;// 找到第一个不符合条件的值,在右边,所以不用减1
}
}
return nums[low];
}
}
71.按权重生成随机数
关键算法:
算法思想:前缀和 + 二分查找。以前缀和来模仿权重的分量,我们可以将第一个数作为起始的长度如:[2,4],那么第一个长度为2,紧接着是占了4个比例的,我们将它变换为坐标,即0-2为0,3-6为1。所以编程了前缀和[2,6],我们在这样的线段中随机选取一个值,第一个大于它的坐标,即为要找的坐标。
关键算法:
class Solution {
int[] pre;
int total;
public Solution(int[] w) {
pre = new int[w.length];
pre[0] = w[0];
for (int i = 1; i < w.length; ++i) { // 求前缀和
pre[i] = pre[i - 1] + w[i];
}
total = Arrays.stream(w).sum(); // 求线段长度
}
public int pickIndex() {
int x = (int) (Math.random() * total) + 1; // 从线段中随机选取一个数
return binarySearch(x);
}
private int binarySearch(int x) {
int low = 0, high = pre.length - 1;
while (low <= high) { // 使用二分查找,选择第一个大于x的值
int mid = (high - low) / 2 + low;
if (pre[mid] < x) { // 小于x则在右边
low = mid + 1;
} else { // 大于x,找到最小的那个
high = mid - 1;
}
}
return low;
}
}
72.求平方根
关键算法:
算法思想:二分查找。找到能够使得mid*mid=x的值。
关键算法:
class Solution {
public int mySqrt(int x) {
int l = 0, r = x, ans = -1;
while (l <= r) {
int mid = l + (r - l) / 2;
if ((long) mid * mid <= x) {
ans = mid;
l = mid + 1;
} else {
r = mid - 1;
}
}
return ans;
}
}
- 狒狒吃香蕉
关键算法:
算法思想:二分查找。最小的速度,首先速度的范围一定是在1-最大值之间,因为如果超过最大值,没啥意义。在1-maxTime,之间进行二分查找,取得一个速度,算出时间,是否是最小时间,如果是,则保存,如果不是则减小速度,继续。
关键算法:
class Solution {
public int minEatingSpeed(int[] piles, int h) {
int low = 1;
int high = 0;
for (int pile : piles) {
high = Math.max(high, pile);
}
int k = high;
while (low <= high) {
int speed = (high - low) / 2 + low;
long time = getTime(piles, speed);
if (time <= h) { // 速度符合,保存,然后减小空间继续找,直到找到最小得那个。
k = speed;
high = speed - 1;
} else {
low = speed + 1;
}
}
return k;
}
public long getTime(int[] piles, int speed) {
long time = 0;
for (int pile : piles) {
int curTime = (pile + speed - 1) / speed;
time += curTime;
}
return time;
}
}
74.合并区间
关键算法:
算法思想:排序。先将数组按第一位排序,如果前一个数的尾巴小于后一个数的开头,则新建一个数组,放入结果中,如果前一个数的尾巴大于等于后一个数的开头,则合并,合并取结果中最后一个数组的尾巴和当前数的尾巴的最大值。
关键算法:
class Solution {
public int[][] merge(int[][] intervals) {
if (intervals.length == 0) {
return new int[0][2];
}
Arrays.sort(intervals, new Comparator<int[]>() { // 按第一位排序
public int compare(int[] interval1, int[] interval2) {
return interval1[0] - interval2[0];
}
});
List<int[]> merged = new ArrayList<int[]>();
for (int i = 0; i < intervals.length; ++i) {
int L = intervals[i][0], R = intervals[i][1]; // 当前数的开头和尾巴
if (merged.size() == 0 || merged.get(merged.size() - 1)[1] < L) {// 第一个数组可以直接放进去,如果小于后面的开头,则新建一个。
merged.add(new int[]{L, R});
} else { // 可以合并,更新结果中最后一个数组的尾巴,取最后一个结果尾巴和当前尾巴的最大值
merged.get(merged.size() - 1)[1] = Math.max(merged.get(merged.size() - 1)[1], R);
}
}
return merged.toArray(new int[merged.size()][]);
}
}
75.数组相对排序
关键算法:
算法思想:自定义排序。将第二个数组放入map中,键取它的值,值取它的索引。从第一个数组中取两个数,如果都不在map中,则按照从小到大排序,如果都在map中,则按照它所在的索引排序,如果只有一个在,则哪个在map中哪个小。
关键算法:
class Solution {
public int[] relativeSortArray(int[] arr1, int[] arr2) {
Map<Integer, Integer> map = new HashMap<>();
int length = arr2.length;
for (int i = 0; i < length; i++) {
map.put(arr2[i], i);
}
return Arrays.stream(arr1).boxed().sorted((i1, i2) -> {
if (map.containsKey(i1) && map.containsKey(i2)) { // 取第二个数组的索引进行比较
return map.get(i1) - map.get(i2);
} else if (map.containsKey(i1)) { // 前面的数在map中,前面的小,位置不变
return -1;
} else if (map.containsKey(i2)) { // 后面的数在map中,后面的小,位置交换
return 1;
} else { // 都不在map中,取他们的差值
return i1 - i2;
}
}).mapToInt(Integer::valueOf).toArray(); // 变成int数组
}
}
- 数组中的第 k 大的数字
关键算法:
算法思想:快速排序。
关键算法:
class Solution {
Random random = new Random();
public int findKthLargest(int[] nums, int k) {
return quickSelect(nums, 0, nums.length - 1, nums.length - k);
}
public int quickSelect(int[] a, int l, int r, int index) {
int q = randomPartition(a, l, r);
if (q == index) {
return a[q];
} else {
return q < index ? quickSelect(a, q + 1, r, index) : quickSelect(a, l, q - 1, index);
}
}
public int randomPartition(int[] a, int l, int r) {
int i = random.nextInt(r - l + 1) + l;
swap(a, i, r);
return partition(a, l, r);
}
public int partition(int[] a, int l, int r) {
int x = a[r], i = l - 1;
for (int j = l; j < r; ++j) {
if (a[j] <= x) {
swap(a, ++i, j);
}
}
swap(a, i + 1, r);
return i + 1;
}
public void swap(int[] a, int i, int j) {
int temp = a[i];
a[i] = a[j];
a[j] = temp;
}
}
方法二:
关键算法:
算法思想:堆排序。
关键算法:
class Solution {
public int findKthLargest(int[] nums, int k) {
int heapSize = nums.length;
buildMaxHeap(nums, heapSize);
for (int i = nums.length - 1; i >= nums.length - k + 1; --i) {
swap(nums, 0, i);
--heapSize;
maxHeapify(nums, 0, heapSize);
}
return nums[0];
}
public void buildMaxHeap(int[] a, int heapSize) {
for (int i = heapSize / 2; i >= 0; --i) {
maxHeapify(a, i, heapSize);
}
}
public void maxHeapify(int[] a, int i, int heapSize) {
int l = i * 2 + 1, r = i * 2 + 2, largest = i;
if (l < heapSize && a[l] > a[largest]) {
largest = l;
}
if (r < heapSize && a[r] > a[largest]) {
largest = r;
}
if (largest != i) {
swap(a, i, largest);
maxHeapify(a, largest, heapSize);
}
}
public void swap(int[] a, int i, int j) {
int temp = a[i];
a[i] = a[j];
a[j] = temp;
}
}
77.链表排序
关键算法:
算法思想:归并排序(递归)。
关键算法:
class Solution {
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;
}
public ListNode merge(ListNode head1, ListNode head2) { // 合并两个链表
ListNode dummyHead = new ListNode(0);
ListNode temp = dummyHead, temp1 = head1, temp2 = head2;
while (temp1 != null && temp2 != null) {
if (temp1.val <= temp2.val) {
temp.next = temp1;
temp1 = temp1.next;
} else {
temp.next = temp2;
temp2 = temp2.next;
}
temp = temp.next;
}
if (temp1 != null) {
temp.next = temp1;
} else if (temp2 != null) {
temp.next = temp2;
}
return dummyHead.next;
}
}
方法二:
关键算法:
算法思想:归并排序(非递归)。
关键算法:
class Solution {
public ListNode sortList(ListNode head) {
if (head == null) {
return head;
}
int length = 0;
ListNode node = head;
while (node != null) {
length++;
node = node.next;
}
ListNode dummyHead = new ListNode(0, head);
for (int subLength = 1; subLength < length; subLength <<= 1) {
ListNode prev = dummyHead, curr = dummyHead.next;
while (curr != null) {
ListNode head1 = curr;
for (int i = 1; i < subLength && curr.next != null; i++) {
curr = curr.next;
}
ListNode head2 = curr.next;
curr.next = null;
curr = head2;
for (int i = 1; i < subLength && curr != null && curr.next != null; i++) {
curr = curr.next;
}
ListNode next = null;
if (curr != null) {
next = curr.next;
curr.next = null;
}
ListNode merged = merge(head1, head2);
prev.next = merged;
while (prev.next != null) {
prev = prev.next;
}
curr = next;
}
}
return dummyHead.next;
}
public ListNode merge(ListNode head1, ListNode head2) {
ListNode dummyHead = new ListNode(0);
ListNode temp = dummyHead, temp1 = head1, temp2 = head2;
while (temp1 != null && temp2 != null) {
if (temp1.val <= temp2.val) {
temp.next = temp1;
temp1 = temp1.next;
} else {
temp.next = temp2;
temp2 = temp2.next;
}
temp = temp.next;
}
if (temp1 != null) {
temp.next = temp1;
} else if (temp2 != null) {
temp.next = temp2;
}
return dummyHead.next;
}
}
78.合并排序链表
关键算法:
算法思想:优先队列。
关键算法:
class Solution {
class Status implements Comparable<Status> {
int val;
ListNode ptr;
Status(int val, ListNode ptr) {
this.val = val;
this.ptr = ptr;
}
public int compareTo(Status status2) { // 进入这个类的值,都会进行排序。
return this.val - status2.val;
}
}
PriorityQueue<Status> queue = new PriorityQueue<Status>();
public ListNode mergeKLists(ListNode[] lists) {
for (ListNode node: lists) { // 将头节点都放进队列中。
if (node != null) {
queue.offer(new Status(node.val, node));
}
}
ListNode head = new ListNode(0); // 新建一个新的节点,用于保存结果
ListNode tail = head;
while (!queue.isEmpty()) { // 队列不为空
Status f = queue.poll(); // 将队头插入结果中
tail.next = f.ptr; //
tail = tail.next; //
if (f.ptr.next != null) { // 如果队头后面还有节点,将节点放到队列中。
queue.offer(new Status(f.ptr.next.val, f.ptr.next));
}
}
return head.next;
}
}
79.所有子集
关键算法:
算法思想:根据位置来进行子集的统计。在总个数上,统计哪个位置上有1,就将哪个位置对应的数添加进结果。
关键算法:
class Solution {
List<Integer> t = new ArrayList<Integer>();
List<List<Integer>> ans = new ArrayList<List<Integer>>();
public List<List<Integer>> subsets(int[] nums) {
int n = nums.length;
for (int mask = 0; mask < (1 << n); ++mask) { // 1左移n个单位,为总数
t.clear();
for (int i = 0; i < n; ++i) {
if ((mask & (1 << i)) != 0) { // 一共有n个数,看他在哪个数上有数字,就将它添加进去。
t.add(nums[i]);
}
}
ans.add(new ArrayList<Integer>(t)); // 将数字添加进去
}
return ans;
}
}
方法二:
关键算法:
算法思想:回溯。
关键算法:
class Solution {
List<Integer> t = new ArrayList<Integer>();
List<List<Integer>> ans = new ArrayList<List<Integer>>();
public List<List<Integer>> subsets(int[] nums) {
dfs(0, nums);
return ans;
}
public void dfs(int cur, int[] nums) {
if (cur == nums.length) {
ans.add(new ArrayList<Integer>(t));
return;
}
t.add(nums[cur]);
dfs(cur + 1, nums);
t.remove(t.size() - 1);
dfs(cur + 1, nums);
}
}
80.含有 k 个元素的组合
关键算法:
算法思想:
关键算法:
class Solution {
List<Integer> temp = new ArrayList<Integer>();
List<List<Integer>> ans = new ArrayList<List<Integer>>();
public List<List<Integer>> combine(int n, int k) {
dfs(1, n, k);
return ans;
}
public void dfs(int cur, int n, int k) {
// 剪枝:temp 长度加上区间 [cur, n] 的长度小于 k,不可能构造出长度为 k 的 temp
if (temp.size() + (n - cur + 1) < k) {
return;
}
// 记录合法的答案
if (temp.size() == k) {
ans.add(new ArrayList<Integer>(temp));
return;
}
// 考虑选择当前位置
temp.add(cur);
dfs(cur + 1, n, k);
temp.remove(temp.size() - 1);
// 考虑不选择当前位置
dfs(cur + 1, n, k);
}
}
81.允许重复选择元素的组合
关键算法:
算法思想:
关键算法:
class Solution {
public List<List<Integer>> combinationSum(int[] candidates, int target) {
List<List<Integer>> ans = new ArrayList<List<Integer>>();
List<Integer> combine = new ArrayList<Integer>();
dfs(candidates, target, ans, combine, 0);
return ans;
}
public void dfs(int[] candidates, int target, List<List<Integer>> ans, List<Integer> combine, int idx) {
if (idx == candidates.length) {
return;
}
if (target == 0) {
ans.add(new ArrayList<Integer>(combine));
return;
}
// 直接跳过
dfs(candidates, target, ans, combine, idx + 1);
// 选择当前数
if (target - candidates[idx] >= 0) {
combine.add(candidates[idx]);
dfs(candidates, target - candidates[idx], ans, combine, idx);
combine.remove(combine.size() - 1);
}
}
}
82.含有重复元素集合的组合
关键算法:
算法思想:按大小统计个数后,排序,从后往前查,递归,找到第一个大于它的数结束。
关键算法:
class Solution {
List<int[]> freq = new ArrayList<int[]>();
List<List<Integer>> ans = new ArrayList<List<Integer>>();
List<Integer> sequence = new ArrayList<Integer>();
public List<List<Integer>> combinationSum2(int[] candidates, int target) {
Arrays.sort(candidates);
for (int num : candidates) {// 统计每个数出现的频次
int size = freq.size();
if (freq.isEmpty() || num != freq.get(size - 1)[0]) {
freq.add(new int[]{num, 1});
} else {
++freq.get(size - 1)[1];
}
}
dfs(0, target);
return ans;
}
public void dfs(int pos, int rest) {
if (rest == 0) {
ans.add(new ArrayList<Integer>(sequence));
return;
}
if (pos == freq.size() || rest < freq.get(pos)[0]) {
return;
}
dfs(pos + 1, rest);
int most = Math.min(rest / freq.get(pos)[0], freq.get(pos)[1]); // 看它需要几个当前的值,最多也就是所有的个数
for (int i = 1; i <= most; ++i) { // 如果满足条件返回来,则已经保存结果。不满足条件回来,将会有两种可以,第一种是当前的个数有几个,如果只有一个,而后面的又不符合的话,则不可能,就会删除这个。
sequence.add(freq.get(pos)[0]);
dfs(pos + 1, rest - i * freq.get(pos)[0]);
}
for (int i = 1; i <= most; ++i) {
sequence.remove(sequence.size() - 1);
}
}
}
83.没有重复元素集合的全排列
关键算法:
算法思想:
关键算法:
class Solution {
public List<List<Integer>> permute(int[] nums) {
List<List<Integer>> res = new ArrayList<List<Integer>>();
List<Integer> output = new ArrayList<Integer>();
for (int num : nums) {
output.add(num);
}
int n = nums.length;
backtrack(n, output, res, 0);
return res;
}
public void backtrack(int n, List<Integer> output, List<List<Integer>> res, int first) {
// 所有数都填完了
if (first == n) {
res.add(new ArrayList<Integer>(output));
}
for (int i = first; i < n; i++) { // 控制交换,从当前索引开始,要和它后面的每一个交换一次
Collections.swap(output, first, i);
backtrack(n, output, res, first + 1);
// 撤销操作
Collections.swap(output, first, i);
}
}
}
84.含有重复元素集合的全排列
class Solution {
public List<List<Integer>> permute(int[] nums) {
List<List<Integer>> res = new ArrayList<List<Integer>>();
List<Integer> output = new ArrayList<Integer>();
for (int num : nums) {
output.add(num);
}
int n = nums.length;
backtrack(n, output, res, 0);
return res;
}
public void backtrack(int n, List<Integer> output, List<List<Integer>> res, int first) {
// 所有数都填完了
if (first == n) {
res.add(new ArrayList<Integer>(output));
}
for (int i = first; i < n; i++) {
// 动态维护数组
Collections.swap(output, first, i);
// 继续递归填下一个数
backtrack(n, output, res, first + 1);
// 撤销操作
Collections.swap(output, first, i);
}
}
}
关键算法:
算法思想:回溯。回溯的含义可以让执行错误,或者执行完成的程序,回到调用它的位置,类似于图遍历中的深度遍历。要插入左括号和右括号,可以使用回溯,对于插错或者满足条件的插入方式进行跳过或者保存,然后回到原来的位置重新插入。
关键算法:
class Solution {
public List<String> generateParenthesis(int n) {
List<String> ans = new ArrayList<String>();
backtrack(ans, new StringBuilder(), 0, 0, n);
return ans;
}
// 回溯的含义是:每一个流程都是进行同样的操作,可以使用一些判断来限制数量,当回溯时,一般需要将之前插入的数删除。然后后序是否需要处理,就看具体情况。所以可以将回溯的方式视为两个流程:第一个流程:每个进行函数的运算逻辑都是一样的;第二个流程回溯应该怎么处理。(一般是判断再进行处理)。
public void backtrack(List<String> ans, StringBuilder cur, int open, int close, int max) { //
if (cur.length() == max * 2) {
ans.add(cur.toString());
return;
}
if (open < max) { // 先插入左括号,通过左括号的数量来判断是否进行插入,当回溯时,也是先从左括号开始
cur.append('(');
backtrack(ans, cur, open + 1, close, max);
cur.deleteCharAt(cur.length() - 1);
}
if (close < open) { // 然后再插入右括号,通过右括号的数量来判断是否进行插入,当回溯时,还是会先判断左括号,然后判断右括号
cur.append(')');
backtrack(ans, cur, open, close + 1, max);
cur.deleteCharAt(cur.length() - 1);
}
}
}
86.分割回文子字符串
关键算法:
算法思想:
关键算法:
class Solution {
int[][] f;
List<List<String>> ret = new ArrayList<List<String>>();
List<String> ans = new ArrayList<String>();
int n;
public List<List<String>> partition(String s) {
n = s.length();
f = new int[n][n];
dfs(s, 0);
return ret;
}
public void dfs(String s, int i) {
if (i == n) {
ret.add(new ArrayList<String>(ans));
return;
}
for (int j = i; j < n; ++j) {
if (isPalindrome(s, i, j) == 1) {
ans.add(s.substring(i, j + 1)); // 回文,可以将这个保存在里面
dfs(s, j + 1); // 依次以当前位置开始,分别判断后面的字符串是否为字串
ans.remove(ans.size() - 1);
}
}
}
// 记忆化搜索中,f[i][j] = 0 表示未搜索,1 表示是回文串,-1 表示不是回文串
public int isPalindrome(String s, int i, int j) {
if (f[i][j] != 0) {
return f[i][j];
}
if (i >= j) {
f[i][j] = 1;
} else if (s.charAt(i) == s.charAt(j)) {
f[i][j] = isPalindrome(s, i + 1, j - 1);
} else {
f[i][j] = -1;
}
return f[i][j];
}
}
87.复原 IP
关键算法:
算法思想:
关键算法:
class Solution {
static final int SEG_COUNT = 4;
List<String> ans = new ArrayList<String>();
int[] segments = new int[SEG_COUNT];
public List<String> restoreIpAddresses(String s) {
segments = new int[SEG_COUNT];
dfs(s, 0, 0);
return ans;
}
public void dfs(String s, int segId, int segStart) {
// 如果找到了 4 段 IP 地址并且遍历完了字符串,那么就是一种答案
if (segId == SEG_COUNT) {
if (segStart == s.length()) {
StringBuffer ipAddr = new StringBuffer();
for (int i = 0; i < SEG_COUNT; ++i) {
ipAddr.append(segments[i]);
if (i != SEG_COUNT - 1) {
ipAddr.append('.');
}
}
ans.add(ipAddr.toString());
}
return;
}
// 如果还没有找到 4 段 IP 地址就已经遍历完了字符串,那么提前回溯
if (segStart == s.length()) {
return;
}
// 由于不能有前导零,如果当前数字为 0,那么这一段 IP 地址只能为 0
if (s.charAt(segStart) == '0') {
segments[segId] = 0;
dfs(s, segId + 1, segStart + 1);
}
// 一般情况,枚举每一种可能性并递归
int addr = 0;
for (int segEnd = segStart; segEnd < s.length(); ++segEnd) {
addr = addr * 10 + (s.charAt(segEnd) - '0');
if (addr > 0 && addr <= 0xFF) {
segments[segId] = addr;
dfs(s, segId + 1, segEnd + 1);
} else {
break;
}
}
}
}
88.爬楼梯的最少成本
关键算法:
算法思想:动态规划。当前值和前面得某些值有关系,并不断地影响后面的值。
关键算法:
class Solution {
public int minCostClimbingStairs(int[] cost) {
int n = cost.length;
int[] dp = new int[n + 1];
dp[0] = dp[1] = 0;
for (int i = 2; i <= n; i++) { // 不记录当前值的值,而是记录那些能够到达这个位置的有哪些值。
dp[i] = Math.min(dp[i - 1] + cost[i - 1], dp[i - 2] + cost[i - 2]);
}
return dp[n];
}
}
方法二:
关键算法:
算法思想:滚动数组。只需要最后的值的结果,其他的可以作为中间结果。可以设置两个值,每次修正第二个值不断地向后移动。
关键算法:
class Solution {
public int minCostClimbingStairs(int[] cost) {
int n = cost.length;
int prev = 0, curr = 0;
for (int i = 2; i <= n; i++) {
int next = Math.min(curr + cost[i - 1], prev + cost[i - 2]);
prev = curr;
curr = next;
}
return curr;
}
}
89.房屋偷盗
关键算法:
算法思想:动态规划。当前值和前面得某些值有关系,并不断地影响后面的值。
关键算法:
class Solution {
public int rob(int[] nums) {
if (nums == null || nums.length == 0) {
return 0;
}
int length = nums.length;
if (length == 1) {
return nums[0];
}
int[] dp = new int[length];
dp[0] = nums[0];
dp[1] = Math.max(nums[0], nums[1]);
for (int i = 2; i < length; i++) {
dp[i] = Math.max(dp[i - 2] + nums[i], dp[i - 1]);
}
return dp[length - 1];
}
}
方法二:
关键算法:
算法思想:动态数组。
关键算法:
class Solution {
public int rob(int[] nums) {
if (nums == null || nums.length == 0) {
return 0;
}
int length = nums.length;
if (length == 1) {
return nums[0];
}
int first = nums[0], second = Math.max(nums[0], nums[1]);
for (int i = 2; i < length; i++) {
int temp = second;
second = Math.max(first + nums[i], second);
first = temp;
}
return second;
}
}
90.环形房屋偷盗
关键算法:
算法思想:分两种情况动态规划。也可以使用动态数组。
关键算法:
class Solution {
public int rob(int[] nums) {
int length = nums.length;
if (length == 1) {
return nums[0];
} else if (length == 2) {
return Math.max(nums[0], nums[1]);
}
return Math.max(robRange(nums, 0, length - 2), robRange(nums, 1, length - 1));
}
public int robRange(int[] nums, int start, int end) {
int first = nums[start], second = Math.max(nums[start], nums[start + 1]);
for (int i = start + 2; i <= end; i++) {
int temp = second;
second = Math.max(first + nums[i], second);
first = temp;
}
return second;
}
}
91.粉刷房子
关键算法:
算法思想:动态规划。也可以使用动态数组。这里是二维的,所以每一种可能会有三种结果,对于每一行的结果的三种可能都取最小值。根据公式:当前的取值是前一行取值中与当前行不在同一列的,即可以使用当前列+1,+2与当前行的长度取余来获得。然后加上当前的值。取最小值,每行有三个。
关键算法:
class Solution {
public int minCost(int[][] costs) {
int n = costs.length;
int[] dp = new int[3];
for (int j = 0; j < 3; j++) {
dp[j] = costs[0][j];
}
for (int i = 1; i < n; i++) {
int[] dpNew = new int[3];
for (int j = 0; j < 3; j++) {
dpNew[j] = Math.min(dp[(j + 1) % 3], dp[(j + 2) % 3]) + costs[i][j];
}
dp = dpNew;
}
return Arrays.stream(dp).min().getAsInt();
}
}
92.翻转字符
关键算法:
算法思想:每个当前值有两种情况,要么翻转为0,即dp[0],要么翻转为1,即dp[1]。如果前面经过反转后(可能是dp[0]也可能是dp[1])已经是有序的了,如果当前值要反转为0,那么要保证有序,前面必须为0,即dp[i][0]=dp[i-1][0]+当前值是否转为0(转则+1,不转则+0);如果当前值要反转为1,那么要前面为0,为1都可以,即dp[i][1]=Min(dp[i-1][0],dp[i-1][1])+当前值是否转为1(转则+1,不转则+0);
关键算法:
class Solution {
public int minFlipsMonoIncr(String s) {
int n = s.length();
int dp0 = 0, dp1 = 0;
for (int i = 0; i < n; i++) {
char c = s.charAt(i);
int dp0New = dp0, dp1New = Math.min(dp0, dp1);
if (c == '1') {// 只有要转为0的需要+1
dp0New++;
} else { // 只有要转为1的需要+1
dp1New++;
}
dp0 = dp0New;
dp1 = dp1New;
}
return Math.min(dp0, dp1);
}
}
93.最长斐波那契数列
关键算法:
算法思想:
关键算法:
class Solution {
public int lenLongestFibSubseq(int[] arr) {
Map<Integer, Integer> indices = new HashMap<Integer, Integer>();
int n = arr.length;
for (int i = 0; i < n; i++) {
indices.put(arr[i], i);
}
int[][] dp = new int[n][n];
int ans = 0;
for (int i = 0; i < n; i++) {
for (int j = i - 1; j >= 0 && arr[j] * 2 > arr[i]; j--) {
int k = indices.getOrDefault(arr[i] - arr[j], -1);
if (k >= 0) {
dp[j][i] = Math.max(dp[k][j] + 1, 3);
}
ans = Math.max(ans, dp[j][i]);
}
}
return ans;
}
}
94.最少回文分割
关键算法:
算法思想:使用一个二维数组记录从i到j是否为一个字串,然后用f[n]表示到当前n为止最多需要切几次,如果在g[0][i]为true,说明从0开始到j本身就是一个字串,不需要被切。如果g[0][i]为false,而在g[j+1][i]处为true,说明从j+1开始到i本身就是一个字串,是可以切的,切的次数就是f[j]+1,然后再比较到达这个位置,哪个最小,取最小值。
关键算法:
class Solution {
public int minCut(String s) {
int n = s.length();
boolean[][] g = new boolean[n][n];
for (int i = 0; i < n; ++i) {
Arrays.fill(g[i], true);
}
for (int i = n - 1; i >= 0; --i) {
for (int j = i + 1; j < n; ++j) {
g[i][j] = s.charAt(i) == s.charAt(j) && g[i + 1][j - 1];
}
}
int[] f = new int[n];
Arrays.fill(f, Integer.MAX_VALUE);
for (int i = 0; i < n; ++i) {
if (g[0][i]) {
f[i] = 0;
} else {
for (int j = 0; j < i; ++j) {
if (g[j + 1][i]) {
f[i] = Math.min(f[i], f[j] + 1);
}
}
}
}
return f[n - 1];
}
}
95.最长公共子序列
关键算法:
算法思想:同样使用二维数组来表示,dp[i][j]表示再i和j之前有多少个最长字串,如果当前的值相等,则在之前的基础上加1,如果不相等,则在保留之前的,一般是比较dp[i - 1][j], dp[i][j - 1]的最大值。
关键算法:
class Solution {
public int longestCommonSubsequence(String text1, String text2) {
int m = text1.length(), n = text2.length();
int[][] dp = new int[m + 1][n + 1];
for (int i = 1; i <= m; i++) {
char c1 = text1.charAt(i - 1);
for (int j = 1; j <= n; j++) {
char c2 = text2.charAt(j - 1);
if (c1 == c2) {
dp[i][j] = dp[i - 1][j - 1] + 1;
} else {
dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]);
}
}
}
return dp[m][n];
}
}
96.字符串交织
关键算法:
算法思想:分别开始遍历两个数组,然后有i和j确定第三个数组的位置,当前值进行判断时,都会因为前面的值是否为true才能进一步。
关键算法:
class Solution {
public boolean isInterleave(String s1, String s2, String s3) {
int n = s1.length(), m = s2.length(), t = s3.length();
if (n + m != t) {
return false;
}
boolean[][] f = new boolean[n + 1][m + 1];
f[0][0] = true;
for (int i = 0; i <= n; ++i) {
for (int j = 0; j <= m; ++j) {
int p = i + j - 1;
if (i > 0) {
f[i][j] = f[i][j] || (f[i - 1][j] && s1.charAt(i - 1) == s3.charAt(p));
}
if (j > 0) {
f[i][j] = f[i][j] || (f[i][j - 1] && s2.charAt(j - 1) == s3.charAt(p));
}
}
}
return f[n][m];
}
}
97.子序列的数目
关键算法: dp[i][j] = dp[i + 1][j + 1] + dp[i + 1][j];
dp[i][j] = dp[i + 1][j];
算法思想:dp[i][j]表示s从i开始到末尾s[i:]有多少个t[j:]的字串,当前值i,j相等时,它的变化是什么呢?首先在i还没有加入的时候,即dp[i+1][j]已经有了这些,即使没有i的加入,至少会有这些,加入了i后,可以同时忽略i和j,因为他俩相等,去掉后的个数和不去掉的个数相等,即dp[i+1][j+1],所以当加入i一共有,dp[i + 1][j + 1] + dp[i + 1][j],如果不相等,那么就是之前的值dp[i + 1][j]。
关键算法:
class Solution {
public int numDistinct(String s, String t) {
int m = s.length(), n = t.length();
if (m < n) {
return 0;
}
int[][] dp = new int[m + 1][n + 1];
for (int i = 0; i <= m; i++) {
dp[i][n] = 1;
}
for (int i = m - 1; i >= 0; i--) {
char sChar = s.charAt(i);
for (int j = n - 1; j >= 0; j--) {
char tChar = t.charAt(j);
if (sChar == tChar) {
dp[i][j] = dp[i + 1][j + 1] + dp[i + 1][j];
} else {
dp[i][j] = dp[i + 1][j];
}
}
}
return dp[0][0];
}
}
98.路径的数目
关键算法:
算法思想:每一个位置的可能性,都是由上一个位置和左边的位置决定的。dp[i][j]=dp[i-1][j]+dp[i][j-1]。
关键算法:
class Solution {
public int uniquePaths(int m, int n) {
int[][] f = new int[m][n];
for (int i = 0; i < m; ++i) {
f[i][0] = 1;
}
for (int j = 0; j < n; ++j) {
f[0][j] = 1;
}
for (int i = 1; i < m; ++i) {
for (int j = 1; j < n; ++j) {
f[i][j] = f[i - 1][j] + f[i][j - 1];
}
}
return f[m - 1][n - 1];
}
}
99.最小路径之和
关键算法:
算法思想:每一个位置的可能性,都是由上一个位置和左边的位置决定的。计算出每个位置的最小值dp[i][j] = Math.min(dp[i - 1][j], dp[i][j - 1]) + grid[i][j];
关键算法:
class Solution {
public int minPathSum(int[][] grid) {
if (grid == null || grid.length == 0 || grid[0].length == 0) {
return 0;
}
int rows = grid.length, columns = grid[0].length;
int[][] dp = new int[rows][columns];
dp[0][0] = grid[0][0];
for (int i = 1; i < rows; i++) {
dp[i][0] = dp[i - 1][0] + grid[i][0];
}
for (int j = 1; j < columns; j++) {
dp[0][j] = dp[0][j - 1] + grid[0][j];
}
for (int i = 1; i < rows; i++) {
for (int j = 1; j < columns; j++) {
dp[i][j] = Math.min(dp[i - 1][j], dp[i][j - 1]) + grid[i][j];
}
}
return dp[rows - 1][columns - 1];
}
}
100.三角形中最小路径之和
关键算法:
算法思想:动态规划,找好转换逻辑。设dp[i][j]表示第i行第j列的路径和。dp[i][0]只能由dp[i-1][0]来决定。dp[i][i]只能由dp[i-1][i-1]决定。其他 dp[i][j] = Math.min(f[i - 1][j - 1], f[i - 1][j]) 决定。
关键算法:
class Solution {
public int minimumTotal(List<List<Integer>> triangle) {
int n = triangle.size();
int[][] f = new int[n][n];
f[0][0] = triangle.get(0).get(0);
for (int i = 1; i < n; ++i) {
f[i][0] = f[i - 1][0] + triangle.get(i).get(0);
for (int j = 1; j < i; ++j) {
f[i][j] = Math.min(f[i - 1][j - 1], f[i - 1][j]) + triangle.get(i).get(j);
}
f[i][i] = f[i - 1][i - 1] + triangle.get(i).get(i);
}
int minTotal = f[n - 1][0];
for (int i = 1; i < n; ++i) {
minTotal = Math.min(minTotal, f[n - 1][i]);
}
return minTotal;
}
}
方法二:
算法思想:动态规划,找好转换逻辑。dp[i]表示第i列的最小值,它每次保存的都是最大行的那个路径和。当i=0时,它只和dp[0]相关,等于上一个dp[0]加上当前值,然后更新dp[0],当i=当前行的最大值时,dp[i]=dp[i-1]加上当前值。其他dp[i]为Math.min(dp[j - 1], dp[j]) + triangle.get(i).get(j);
关键算法:
class Solution {
public int minimumTotal(List<List<Integer>> triangle) {
int n = triangle.size();
int[] f = new int[n];
f[0] = triangle.get(0).get(0);
for (int i = 1; i < n; ++i) {
f[i] = f[i - 1] + triangle.get(i).get(i);
for (int j = i - 1; j > 0; --j) {
f[j] = Math.min(f[j - 1], f[j]) + triangle.get(i).get(j);
}
f[0] += triangle.get(i).get(0);
}
int minTotal = f[0];
for (int i = 1; i < n; ++i) {
minTotal = Math.min(minTotal, f[i]);
}
return minTotal;
}
}
101.分割等和子集
算法思想:求出最大值和平均值。dp[i][j] 表示从数组的 [0,i][0,i][0,i] 下标范围内选取若干个正整数,是否存在一种选取方案使得被选取的正整数的和等于j。j≥nums[i],dp[i][j]=dp[i−1][j] ∣ dp[i−1][j−nums[i]]。j<nums[i],dp[i][j]=dp[i−1][j]。
关键算法:
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];
}
}
方法二:
算法思想:求出最大值和平均值。dp[i]表示第i列之前的和为当前target是否存在,它取决于之前的dp[i-1]和当前值target,dp[j-num[i]],所以dp[j] |= dp[j - num];为避免j-num已经更新,从后向前遍历。
关键算法:
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[target + 1];
dp[0] = true;
for (int i = 0; i < n; i++) {
int num = nums[i];
for (int j = target; j >= num; --j) {
dp[j] |= dp[j - num];
}
}
return dp[target];
}
}
102.加减的目标值
算法思想:回溯。
关键算法:
class Solution {
int count = 0;
public int findTargetSumWays(int[] nums, int target) {
backtrack(nums, target, 0, 0);
return count;
}
public void backtrack(int[] nums, int target, int index, int sum) {
if (index == nums.length) {
if (sum == target) {
count++;
}
} else {
backtrack(nums, target, index + 1, sum + nums[index]);
backtrack(nums, target, index + 1, sum - nums[index]);
}
}
}
方法二:动态规划
算法思想:动态规划。
关键算法:
class Solution {
public int findTargetSumWays(int[] nums, int target) {
int sum = 0;
for (int num : nums) {
sum += num;
}
int diff = sum - target;
if (diff < 0 || diff % 2 != 0) {
return 0;
}
int n = nums.length, neg = diff / 2;
int[][] dp = new int[n + 1][neg + 1];
dp[0][0] = 1;
for (int i = 1; i <= n; i++) {
int num = nums[i - 1];
for (int j = 0; j <= neg; j++) {
dp[i][j] = dp[i - 1][j];
if (j >= num) {
dp[i][j] += dp[i - 1][j - num];
}
}
}
return dp[n][neg];
}
}
103.最少的硬币数目
关键算法:
算法思想:动态规划。dp[i]表示金额i所具有最小的足额和,后面的金额都是在这个基础上再加上对应的面值,所以dp[i] = Math.min(dp[i], dp[i - coins[j]] + 1)。
关键算法:
public class Solution {
public int coinChange(int[] coins, int amount) {
int max = amount + 1;
int[] dp = new int[amount + 1];
Arrays.fill(dp, max);
dp[0] = 0;
for (int i = 1; i <= amount; i++) {
for (int j = 0; j < coins.length; j++) {
if (coins[j] <= i) {
dp[i] = Math.min(dp[i], dp[i - coins[j]] + 1);
}
}
}
return dp[amount] > amount ? -1 : dp[amount];
}
}
104.排列的数目
关键算法:
算法思想:动态规划。dp[i]代表金额i一共有几种组合方式。是基于前面的累加,dp[i] += dp[i - num];
关键算法:
class Solution {
public int combinationSum4(int[] nums, int target) {
int[] dp = new int[target + 1];
dp[0] = 1;
for (int i = 1; i <= target; i++) {
for (int num : nums) {
if (num <= i) {
dp[i] += dp[i - num];
}
}
}
return dp[target];
}
}
105.岛屿的最大面积
关键算法:
算法思想:深度遍历。使用深度遍历,遍历每个节点,通过上下左右移动,统计1的个数。统计过的赋0。
关键算法:
class Solution {
public int maxAreaOfIsland(int[][] grid) {
int ans = 0;
for (int i = 0; i != grid.length; ++i) {
for (int j = 0; j != grid[0].length; ++j) {
ans = Math.max(ans, dfs(grid, i, j));
}
}
return ans;
}
public int dfs(int[][] grid, int cur_i, int cur_j) {
if (cur_i < 0 || cur_j < 0 || cur_i == grid.length || cur_j == grid[0].length || grid[cur_i][cur_j] != 1) {
return 0;
}
grid[cur_i][cur_j] = 0;
int[] di = {0, 0, 1, -1};
int[] dj = {1, -1, 0, 0};
int ans = 1;
for (int index = 0; index != 4; ++index) {
int next_i = cur_i + di[index], next_j = cur_j + dj[index];
ans += dfs(grid, next_i, next_j);
}
return ans;
}
}
106.二分图
关键算法:
算法思想:广度遍历。用颜色标记奇数层如果和偶数层的颜色不一致就可以,也就是说相邻的两层颜色要不一致。
关键算法:
class Solution {
private static final int UNCOLORED = 0;
private static final int RED = 1;
private static final int GREEN = 2;
private int[] color;
public boolean isBipartite(int[][] graph) {
int n = graph.length;
color = new int[n];
Arrays.fill(color, UNCOLORED);
for (int i = 0; i < n; ++i) {
if (color[i] == UNCOLORED) {
Queue<Integer> queue = new LinkedList<Integer>();
queue.offer(i);
color[i] = RED;
while (!queue.isEmpty()) {
int node = queue.poll();
int cNei = color[node] == RED ? GREEN : RED;
for (int neighbor : graph[node]) {
if (color[neighbor] == UNCOLORED) {
queue.offer(neighbor);
color[neighbor] = cNei;
} else if (color[neighbor] != cNei) {
return false;
}
}
}
}
}
return true;
}
}
107.矩阵中的距离
关键算法:
算法思想:广度遍历。0周围没被访问过的都是1,1周围没被访问过的都是1+1。将所有0放进队列中,0周围赋值1也放进去,然后1周围放1+1。以此类推。
关键算法:
class Solution {
static int[][] dirs = {{-1, 0}, {1, 0}, {0, -1}, {0, 1}};
public int[][] updateMatrix(int[][] matrix) {
int m = matrix.length, n = matrix[0].length;
int[][] dist = new int[m][n];
boolean[][] seen = new boolean[m][n];
Queue<int[]> queue = new LinkedList<int[]>();
// 将所有的 0 添加进初始队列中
for (int i = 0; i < m; ++i) {
for (int j = 0; j < n; ++j) {
if (matrix[i][j] == 0) {
queue.offer(new int[]{i, j});
seen[i][j] = true;
}
}
}
// 广度优先搜索
while (!queue.isEmpty()) {
int[] cell = queue.poll();
int i = cell[0], j = cell[1];
for (int d = 0; d < 4; ++d) {
int ni = i + dirs[d][0];
int nj = j + dirs[d][1];
if (ni >= 0 && ni < m && nj >= 0 && nj < n && !seen[ni][nj]) {
dist[ni][nj] = dist[i][j] + 1;
queue.offer(new int[]{ni, nj});
seen[ni][nj] = true;
}
}
}
return dist;
}
}
关键算法:
算法思想:创建图+搜索。在节点之间使用了虚拟节点,来方便找有哪些节点是经过一次变换。遍历找到start和end,长度/2即为转换次数。
关键算法:
class Solution {
Map<String, Integer> wordId = new HashMap<String, Integer>();
List<List<Integer>> edge = new ArrayList<List<Integer>>();
int nodeNum = 0;
public int ladderLength(String beginWord, String endWord, List<String> wordList) {
for (String word : wordList) {
addEdge(word);
}
addEdge(beginWord);
if (!wordId.containsKey(endWord)) {
return 0;
}
int[] dis = new int[nodeNum];
Arrays.fill(dis, Integer.MAX_VALUE);
int beginId = wordId.get(beginWord), endId = wordId.get(endWord);
dis[beginId] = 0;
Queue<Integer> que = new LinkedList<Integer>();
que.offer(beginId);
while (!que.isEmpty()) {
int x = que.poll();
if (x == endId) {
return dis[endId] / 2 + 1;
}
for (int it : edge.get(x)) {
if (dis[it] == Integer.MAX_VALUE) {
dis[it] = dis[x] + 1;
que.offer(it);
}
}
}
return 0;
}
public void addEdge(String word) {
addWord(word);
int id1 = wordId.get(word);
char[] array = word.toCharArray();
int length = array.length;
for (int i = 0; i < length; ++i) {
char tmp = array[i];
array[i] = '*';
String newWord = new String(array);
addWord(newWord);
int id2 = wordId.get(newWord);
edge.get(id1).add(id2);
edge.get(id2).add(id1);
array[i] = tmp;
}
}
public void addWord(String word) {
if (!wordId.containsKey(word)) {
wordId.put(word, nodeNum++);
edge.add(new ArrayList<Integer>());
}
}
}
109.开密码锁
关键算法:
算法思想:广度搜索。使用队列保存当前层的所有值,一次遍历所有值,如果都不符合,就继续遍历下一层。
关键算法:
class Solution {
public int openLock(String[] deadends, String target) {
if ("0000".equals(target)) {
return 0;
}
Set<String> dead = new HashSet<String>();
for (String deadend : deadends) {
dead.add(deadend);
}
if (dead.contains("0000")) {
return -1;
}
int step = 0;
Queue<String> queue = new LinkedList<String>();
queue.offer("0000");
Set<String> seen = new HashSet<String>();
seen.add("0000");
while (!queue.isEmpty()) { // 当前层
++step; // step加一
int size = queue.size();
for (int i = 0; i < size; ++i) { // 遍历当前层
String status = queue.poll();
for (String nextStatus : get(status)) { //每个元素旋转一次的可能值,遍历
if (!seen.contains(nextStatus) && !dead.contains(nextStatus)) { // 没访问过且不在死亡锁中
if (nextStatus.equals(target)) {
return step;
}
queue.offer(nextStatus);// 将没访问过的且不在死亡锁中的数放到队列中
seen.add(nextStatus);// 标记
}
}
}
}
return -1;
}
public char numPrev(char x) {
return x == '0' ? '9' : (char) (x - 1);
}
public char numSucc(char x) {
return x == '9' ? '0' : (char) (x + 1);
}
// 枚举 status 通过一次旋转得到的数字
public List<String> get(String status) {
List<String> ret = new ArrayList<String>();
char[] array = status.toCharArray();
for (int i = 0; i < 4; ++i) {
char num = array[i];
array[i] = numPrev(num);
ret.add(new String(array));
array[i] = numSucc(num);
ret.add(new String(array));
array[i] = num;
}
return ret;
}
}
110.所有路径
关键算法:
算法思想:深度搜索。使用栈保存已经访问过的节点,当返回时,出栈,遍历下一节点。
关键算法:
class Solution {
List<List<Integer>> ans = new ArrayList<List<Integer>>();
Deque<Integer> stack = new ArrayDeque<Integer>();
public List<List<Integer>> allPathsSourceTarget(int[][] graph) {
stack.offerLast(0);
dfs(graph, 0, graph.length - 1);
return ans;
}
public void dfs(int[][] graph, int x, int n) {
if (x == n) {
ans.add(new ArrayList<Integer>(stack));
return;
}
for (int y : graph[x]) {
stack.offerLast(y);
dfs(graph, y, n);
stack.pollLast();
}
}
}
111.计算除法
关键算法:
算法思想:广度优先搜索。
关键算法:
class Solution {
public double[] calcEquation(List<List<String>> equations, double[] values, List<List<String>> queries) {
int nvars = 0;
Map<String, Integer> variables = new HashMap<String, Integer>();
int n = equations.size();
for (int i = 0; i < n; i++) {
if (!variables.containsKey(equations.get(i).get(0))) {
variables.put(equations.get(i).get(0), nvars++);
}
if (!variables.containsKey(equations.get(i).get(1))) {
variables.put(equations.get(i).get(1), nvars++);
}
}
// 对于每个点,存储其直接连接到的所有点及对应的权值
List<Pair>[] edges = new List[nvars];
for (int i = 0; i < nvars; i++) {
edges[i] = new ArrayList<Pair>();
}
for (int i = 0; i < n; i++) {
int va = variables.get(equations.get(i).get(0)), vb = variables.get(equations.get(i).get(1));
edges[va].add(new Pair(vb, values[i]));
edges[vb].add(new Pair(va, 1.0 / values[i]));
}
int queriesCount = queries.size();
double[] ret = new double[queriesCount];
for (int i = 0; i < queriesCount; i++) {
List<String> query = queries.get(i);
double result = -1.0;
if (variables.containsKey(query.get(0)) && variables.containsKey(query.get(1))) {
int ia = variables.get(query.get(0)), ib = variables.get(query.get(1));
if (ia == ib) {
result = 1.0;
} else {
Queue<Integer> points = new LinkedList<Integer>();
points.offer(ia);
double[] ratios = new double[nvars];
Arrays.fill(ratios, -1.0);
ratios[ia] = 1.0;
while (!points.isEmpty() && ratios[ib] < 0) {
int x = points.poll();
for (Pair pair : edges[x]) {
int y = pair.index;
double val = pair.value;
if (ratios[y] < 0) {
ratios[y] = ratios[x] * val;
points.offer(y);
}
}
}
result = ratios[ib];
}
}
ret[i] = result;
}
return ret;
}
}
class Pair {
int index;
double value;
Pair(int index, double value) {
this.index = index;
this.value = value;
}
}
112.最长递增路径
关键算法:
算法思想:深度优先搜索。从0,0开始,找一个递增序列,会记下从每个节点开始的递增序列,所以在下次以其他节点为开始的时候,就可以直接返回,如果没有被访问过,说明是从来没有经过的节点,则以它为开始去寻找一个递增序列,如果找打了一个已经有值的序列,则说明以这个值开始的序列已经统计过了,可以直接返回。
关键算法:
class Solution {
public int[][] dirs = {{-1, 0}, {1, 0}, {0, -1}, {0, 1}};
public int rows, columns;
public int longestIncreasingPath(int[][] matrix) {
if (matrix == null || matrix.length == 0 || matrix[0].length == 0) {
return 0;
}
rows = matrix.length;
columns = matrix[0].length;
int[][] memo = new int[rows][columns];
int ans = 0;
for (int i = 0; i < rows; ++i) {
for (int j = 0; j < columns; ++j) {
ans = Math.max(ans, dfs(matrix, i, j, memo));
}
}
return ans;
}
public int dfs(int[][] matrix, int row, int column, int[][] memo) {
if (memo[row][column] != 0) {
return memo[row][column];
}
++memo[row][column];
for (int[] dir : dirs) {
int newRow = row + dir[0], newColumn = column + dir[1];
if (newRow >= 0 && newRow < rows && newColumn >= 0 && newColumn < columns && matrix[newRow][newColumn] > matrix[row][column]) {
memo[row][column] = Math.max(memo[row][column], dfs(matrix, newRow, newColumn, memo) + 1);
}
}
return memo[row][column];
}
}
113.课程顺序
关键算法:
算法思想:拓扑排序。
关键算法:
class Solution {
// 存储有向图
List<List<Integer>> edges;
// 存储每个节点的入度
int[] indeg;
// 存储答案
int[] result;
// 答案下标
int index;
public int[] findOrder(int numCourses, int[][] prerequisites) {
edges = new ArrayList<List<Integer>>();
for (int i = 0; i < numCourses; ++i) {
edges.add(new ArrayList<Integer>());
}
indeg = new int[numCourses];
result = new int[numCourses];
index = 0;
for (int[] info : prerequisites) {
edges.get(info[1]).add(info[0]); // 第二个位置的下一层第一个位置
++indeg[info[0]]; // 第一个位置的入度加1
}
Queue<Integer> queue = new LinkedList<Integer>();
// 将所有入度为 0 的节点放入队列中
for (int i = 0; i < numCourses; ++i) {
if (indeg[i] == 0) {
queue.offer(i);
}
}
while (!queue.isEmpty()) {
// 从队首取出一个节点
int u = queue.poll();
// 放入答案中
result[index++] = u;
for (int v: edges.get(u)) { // 以u位置为入度的节点入度减一
--indeg[v];
// 如果相邻节点 v 的入度为 0,就可以选 v 对应的课程了
if (indeg[v] == 0) {
queue.offer(v);
}
}
}
if (index != numCourses) {
return new int[0];
}
return result;
}
}
114.外星文字典
关键算法:
算法思想:拓扑排序+广度搜索。
关键算法:
class Solution {
Map<Character, List<Character>> edges = new HashMap<Character, List<Character>>();
Map<Character, Integer> indegrees = new HashMap<Character, Integer>();
boolean valid = true;
public String alienOrder(String[] words) {
int length = words.length;
for (String word : words) {
int wordLength = word.length();
for (int j = 0; j < wordLength; j++) {
char c = word.charAt(j);
edges.putIfAbsent(c, new ArrayList<Character>());
}
}
for (int i = 1; i < length && valid; i++) {
addEdge(words[i - 1], words[i]);
}
if (!valid) {
return "";
}
Queue<Character> queue = new ArrayDeque<Character>();
Set<Character> letterSet = edges.keySet();
for (char u : letterSet) {
if (!indegrees.containsKey(u)) {
queue.offer(u);
}
}
StringBuffer order = new StringBuffer();
while (!queue.isEmpty()) {
char u = queue.poll();
order.append(u);
List<Character> adjacent = edges.get(u);
for (char v : adjacent) {
indegrees.put(v, indegrees.get(v) - 1);
if (indegrees.get(v) == 0) {
queue.offer(v);
}
}
}
return order.length() == edges.size() ? order.toString() : "";
}
public void addEdge(String before, String after) {
int length1 = before.length(), length2 = after.length();
int length = Math.min(length1, length2);
int index = 0;
while (index < length) {
char c1 = before.charAt(index), c2 = after.charAt(index);
if (c1 != c2) {
edges.get(c1).add(c2);
indegrees.put(c2, indegrees.getOrDefault(c2, 0) + 1);
break;
}
index++;
}
if (index == length && length1 > length2) {
valid = false;
}
}
}
115.重建序列
关键算法:
算法思想:拓扑排序。
关键算法:
class Solution {
public boolean sequenceReconstruction(int[] nums, int[][] sequences) {
int n = nums.length;
int[] indegrees = new int[n + 1];
Set<Integer>[] graph = new Set[n + 1];
for (int i = 1; i <= n; i++) {
graph[i] = new HashSet<Integer>();
}
for (int[] sequence : sequences) { // 开始画图
int size = sequence.length;
for (int i = 1; i < size; i++) {
int prev = sequence[i - 1], next = sequence[i];
if (graph[prev].add(next)) { // 从该点出发的值到另外一个值
indegrees[next]++; // 入度加1
}
}
}
Queue<Integer> queue = new ArrayDeque<Integer>();
for (int i = 1; i <= n; i++) {
if (indegrees[i] == 0) { // 入度为0的放入队列中
queue.offer(i);
}
}
while (!queue.isEmpty()) { // 队列非空
if (queue.size() > 1) { // 队列大于1说明入度为0的不止一个,返回
return false;
}
int num = queue.poll(); // 出队
Set<Integer> set = graph[num]; // 准备对以它为入点的节点进行操作
for (int next : set) { // 遍历
indegrees[next]--; // 入度减1
if (indegrees[next] == 0) { // 入度为0,放入队列
queue.offer(next);
}
}
}
return true;
}
}
116.省份数量
关键算法:
算法思想:广度遍历。通常的遍历,当会从一个几点都遍历完了之后,还有没有遍历的点,说明是另一个省份了。
关键算法:
class Solution {
public int findCircleNum(int[][] isConnected) {
int cities = isConnected.length;
boolean[] visited = new boolean[cities];
int provinces = 0;
Queue<Integer> queue = new LinkedList<Integer>();
for (int i = 0; i < cities; i++) {
if (!visited[i]) {
queue.offer(i);
while (!queue.isEmpty()) {
int j = queue.poll();
visited[j] = true;
for (int k = 0; k < cities; k++) {
if (isConnected[j][k] == 1 && !visited[k]) {
queue.offer(k);
}
}
}
provinces++;
}
}
return provinces;
}
}
117.相似的字符串
关键算法:
算法思想:如果是这个转换成的字符串,那么将这个字符串的下标设置成它转换的下标,如果这个数据的下标没有改变,说明有数据可以转换成它。
关键算法:
class Solution {
int[] f;
public int numSimilarGroups(String[] strs) {
int n = strs.length;
int m = strs[0].length();
f = new int[n];
for (int i = 0; i < n; i++) {
f[i] = i;
}
for (int i = 0; i < n; i++) {
for (int j = i + 1; j < n; j++) {
int fi = find(i), fj = find(j);
if (fi == fj) {
continue;
}
if (check(strs[i], strs[j], m)) {
f[fi] = fj;
}
}
}
int ret = 0;
for (int i = 0; i < n; i++) {
if (f[i] == i) {
ret++;
}
}
return ret;
}
public int find(int x) {
return f[x] == x ? x : (f[x] = find(f[x]));
}
public boolean check(String a, String b, int len) {
int num = 0;
for (int i = 0; i < len; i++) {
if (a.charAt(i) != b.charAt(i)) {
num++;
if (num > 2) {
return false;
}
}
}
return true;
}
}
118.多余的边
关键算法:
算法思想:寻找连通,连通的意思就是从一个节点出发,一直相连,如果此时从另一个节点出发,发现他们最后连接的都是一样的,说明他们在同一个联通里。所以套娃,如果不相连,但是有,jiujia。parent[find(parent, index1)] = find(parent, index2);
关键算法:
class Solution {
public int[] findRedundantConnection(int[][] edges) {
int n = edges.length;
int[] parent = new int[n + 1];
for (int i = 1; i <= n; i++) {
parent[i] = i;
}
for (int i = 0; i < n; i++) {
int[] edge = edges[i];
int node1 = edge[0], node2 = edge[1];
if (find(parent, node1) != find(parent, node2)) {//
union(parent, node1, node2);
} else {
return edge;
}
}
return new int[0];
}
public void union(int[] parent, int index1, int index2) {
parent[find(parent, index1)] = find(parent, index2);
}
public int find(int[] parent, int index) {
if (parent[index] != index) {
parent[index] = find(parent, parent[index]);
}
return parent[index];
}
}
119.最长连续序列
关键算法:
算法思想:哈希表。bi
关键算法:
class Solution {
public int longestConsecutive(int[] nums) {
Set<Integer> num_set = new HashSet<Integer>();
for (int num : nums) {
num_set.add(num);
}
int longestStreak = 0;
for (int num : num_set) {
if (!num_set.contains(num - 1)) {
int currentNum = num;
int currentStreak = 1;
while (num_set.contains(currentNum + 1)) {
currentNum += 1;
currentStreak += 1;
}
longestStreak = Math.max(longestStreak, currentStreak);
}
}
return longestStreak;
}
}