There are a bunch of problems in Leetcode about removing some characters / digits in a string to make it lexicographically smallest. Here i will have a discussion about how to approach these kind of problems.
Leetcode 316. Remove Duplicate Letters
Leetcode 402. Remove K Digits
Leetcode 321. Create Maximum Number
Leetcode 316. Remove Duplicate Letters
It is possible to traverse every possible permutation of the occured characters and find the smallest one that is a subsequence of the original string, but that will takes O(N!) time which is not fast enough. We can try to construct such string from the start. Traversing characters of the string from left to right and for each character, we will need to decide whether to dump it or take it. There are two important facts to prove here:
Fact 1: If the current character has already occurred before, we should dump this current character instead of removing the previously occurred one.
Fact 2: If the current character is strictly less than its predecessor, we should remove that predecessor as long as it can occur again later.
Proof of fact 1:
Assume the current character is ‘b’, since it has already occurred before, the string can be like this:
A b C b D (A and C are substrings before and after firsts ‘b’ and D is substring after current ‘b’)
If we remove the first character ‘b’, we get A C b D
If we remove the current character ‘b’, we get A b C D
Clearly, A b C D is smaller than A C b D since the first character of C is larger than ‘b’.
Proof of fact 2:
Assume the current character is ‘b’ and its predecessor is ‘a’, the string can be like
A a b D
Removing ‘a’ we will get A b E a F since ‘a’ will occur again.
Clearly, A b E a F is smaller
class Solution {
public String removeDuplicateLetters(String s) {
// This array indicates the count of characters that havent' been dumped.
int[] cnt = new int[26];
for (char c : s.toCharArray()) {
cnt[c - 'a']++;
}
// This array indicates whether a specific character has occurred before.
boolean[] used = new boolean[26];
LinkedList<Character> st = new LinkedList<>();
for (char c : s.toCharArray()) {
// According to fact 1, dump current character if it has occurred before
if (used[c - 'a']) {
cnt[c - 'a']--;
continue;
}
/**
* According to fact 2, cnt[st.peekLast() - 'a'] > 1 ensures the character
* to be removed can occur again later.
*/
while (!st.isEmpty() && st.peekLast() > c && cnt[st.peekLast() - 'a'] > 1) {
cnt[st.peekLast() - 'a']--;
used[st.pollLast() - 'a'] = false;
}
st.addLast(c);
used[c - 'a'] = true;
}
StringBuilder sb = new StringBuilder();
for (char c : st) {
sb.append(c);
}
return sb.toString();
}
}
T
i
m
e
:
O
(
N
)
Time: O(N)
Time:O(N)
S
p
a
c
e
:
O
(
1
)
Space:O(1)
Space:O(1)
Leetcode 402. Remove K Digits
The naive solution is to traverse every possible string after removing k digits, totally ( N K N \atop K KN) possibilities which is not efficient enough.
We can still try contructing the string from start. Traversing the characters of the number string from left to right. A fact similar to fact 2 in Leetcode 316 can be useful:
Fact 1: If the current number is smaller than its predecessor, we should remove that predecessor if we havent’ already deleted K characters.
The proof of this fact is similar to that in previous section.
class Solution {
public String removeKdigits(String num, int k) {
LinkedList<Character> stk = new LinkedList<>();
for (char c : num.toCharArray()) {
// According to fact 1.
while (!stk.isEmpty() && k > 0 && stk.peekLast() > c) {
stk.pollLast();
k--;
}
stk.addLast(c);
}
/**
* It is totally possible that the characters we have visited are in non-decreasing order
* and we haven't deleted enough characters yet. In this case, we should remove the largest
* characters stayed in the stack, which are the k characters from the end of stack.
*/
for (; k > 0; --k) {
stk.pollLast();
}
StringBuilder sb = new StringBuilder();
boolean hasLeading = true;
for (char c : stk) {
if (c == '0' && hasLeading) {
continue;
}
hasLeading = false;
sb.append(c);
}
return sb.length() == 0 ? "0" : sb.toString();
}
}
T
i
m
e
:
O
(
N
)
Time: O(N)
Time:O(N)
S
p
a
c
e
:
O
(
N
)
Space:O(N)
Space:O(N)
Leetcode 321. Create Maximum Number
It is almost the same as the last problem except that there are two arrays with totally K elements selected. There are K + 1 possibilities to split K elements into two arrays with the first array having L 1 ∈ [ 0 , K ] L^{}_{1} \isin \left[ {0, K} \right] L1∈[0,K] elements and the second array having K − L 1 K - L^{}_{1} K−L1 elements.
With the same logic as the last problem, it is easy to calculate the array with L 1 L^{}_{1} L1 and K − L 1 K - L^{}_{1} K−L1 elements which takes O ( m + n ) O(m + n) O(m+n) time. Then here comes the headache: How to merge two arrays into one array with the maximum number? The solution is sort of like merging two sorted arrays, in which we maintain two pointers of each array and move the pointer with larger value forward. The difference is, if two pointers holds the same value, in merging two sorted arrays problem, we can just move any pointer, but in our problem, we cannot just do that since the array is not sorted, it is necessary to keep comparing their next element, or even next next element and etc, which takes O ( m i n ( m , n ) ) O(min(m, n)) O(min(m,n)) time to know which one is larger and then move the respective pointer. So the merging part actually will take O ( ( m + n ) × m i n ( m , n ) ) O((m + n) \times min(m, n)) O((m+n)×min(m,n)) time.
class Solution {
public int[] maxNumber(int[] nums1, int[] nums2, int k) {
int[] ans = new int[k];
for (int l1 = Math.max(k - nums2.length, 0); l1 <= Math.min(nums1.length, k); ++l1) {
int l2 = k - l1;
int[] arr1 = largestNumWithKDigits(nums1, l1);
int[] arr2 = largestNumWithKDigits(nums2, l2);
int[] p = merge(arr1, arr2);
if (compareArray(p, 0, ans, 0) > 0) {
ans = p;
}
}
return ans;
}
private int[] largestNumWithKDigits(int[] nums, int k) {
int[] ret = new int[nums.length];
k = nums.length - k;
int idx = 0;
for (int num : nums) {
while (idx > 0 && ret[idx - 1] < num && k > 0) {
idx--;
k--;
}
ret[idx++] = num;
}
for (; k > 0; --k) {
idx--;
}
return Arrays.copyOfRange(ret, 0, idx);
}
private int[] merge(int[] p1, int[] p2) {
int[] ret = new int[p1.length + p2.length];
int i = 0;
int j = 0;
for (int idx = 0; idx < ret.length; ++idx) {
if (j == p2.length || i < p1.length && compareArray(p1, i, p2, j) >= 0) {
ret[idx] = p1[i++];
} else {
ret[idx] = p2[j++];
}
}
return ret;
}
private int compareArray(int[] p1, int i, int[] p2, int j) {
while (i < p1.length && j < p2.length) {
if (p1[i] != p2[j]) {
return p1[i] - p2[j];
}
i++;
j++;
}
if (i < p1.length) {
return 1;
}
if (j < p2.length) {
return -1;
}
return 0;
}
}
T
i
m
e
:
O
(
K
2
×
(
m
+
n
)
×
m
i
n
(
m
,
n
)
)
Time: \space O(K^{2}_{} \times (m + n) \times min(m, n))
Time: O(K2×(m+n)×min(m,n))
S
p
a
c
e
:
O
(
m
+
n
)
Space: \space O(m + n)
Space: O(m+n)