剑指 offer——数组篇
3. 数组中重复的数字
题意:详见面试题03. 数组中重复的数字
思路1:使用Hash表。遍历整个数组,在访问到某个数字时,先在Hash表中查询是否已包含此数字,如果包含,返回此数字即可;如果不包含,则将数字加到Hash表中,继续遍历下一个数字。
class Solution {
public int findRepeatNumber(int[] nums) {
Set<Integer> set = new HashSet<>();
for (int i : set) {
if (set.contains(i)) {
return i;
} else {
set.add(i);
}
}
return -1;
}
}
思路2:由于数组的大小为n,数字范围为[0,n-1]。我们可以在遍历中将数字换到数组中下标对应的位置,即将0换到数组index为0的位置。如果对应的位置已经有数字了,表示当前遍历的数字是重复的,那么返回当前的数字。
class Solution {
public int findRepeatNumber(int[] nums) {
for (int i = 0; i < nums.length; i ++) {
if (nums[i] != i && nums[i] == nums[nums[i]]) {
return nums[i];
}
while (nums[i] != i && nums[i] != nums[nums[i]]) {
swap(nums, i, nums[i]);
}
}
return -1;
}
private void swap(int[] nums, int i, int j) {
int tmp = nums[i];
nums[i] = nums[j];
nums[j] = tmp;
}
}
4. 二维数组中的查找
题意:详见面试题04. 二维数组中的查找
思路:(1)从左到右、从上到下都是递增关系
(2)搜索过程如果是从左上向右下(或从右下向左上),那么在当前值比目标值小时,将要搜索两个方向(向右和向下的元素都比当前值要大),所以此法将可能遍历整个二维数组,时间复杂度O(n*m)
(3)可以从右上角or左下角开始搜索,每次与target比较后,只有一个方向可以搜索,时间复杂度O(m+n)
class Solution {
public boolean findNumberIn2DArray(int[][] matrix, int target) {
int h = matrix.length;
if (h == 0) {
return false;
}
int w = matrix[0].length;
if (w == 0) {
return false;
}
int i = 0, j = w - 1;
while (i < h && j >= 0) {
if (matrix[i][j] == target) {
return true;
} else if (matrix[i][j] > target) {
j --;
} else {
i ++;
}
}
return false;
}
}
11. 旋转数组的最小数字
题意:面试题11. 旋转数组的最小数字
思路:使用二分的思想,每次将搜素范围缩小。
假设[left, right]为当前的搜索范围。取中间的位置mid,与right位置的数字比较:
1)如果mid位置的数字 > right位置的数字,那么最小数字一定在搜索范围的后半段,所以需要将left置为mid+1;
2)如果mid位置的数字 < right位置的数字,那么最小数字一定在搜索范围的前半段(包括mid位置),所以需要将right置为mid;
3)如果mid位置的数字 = right位置的数字,此时无法判断最小数字的位置,所以只能逐步缩小搜索范围,将right--。
class Solution {
public int minArray(int[] numbers) {
int left = 0;
int right = numbers.length - 1;
int mid;
while (left < right) {
mid = left + ((right - left) >> 1);
if (numbers[mid] > numbers[right]) {
left = mid + 1;
} else if (numbers[mid] < numbers[right]){
right = mid;
} else {
right --;
}
}
return numbers[left];
}
}
17. 打印从1到最大的n位数
题意:面试题17. 打印从1到最大的n位数
思路:最大的n位数加上1一定是1+n个0,所以题目给出了n,就能计算出10的n次方的值,并以此值作为打印的上边界。
class Solution {
public int[] printNumbers(int n) {
int ten = 1;
int i = 1;
while (i <= n) {
ten *= 10;
i ++;
}
int[] res = new int[ten - 1];
for (i = 0; i < res.length; i ++) {
res[i] = i + 1;
}
return res;
}
}
21. 调整数组顺序使奇数位于偶数前面
题意:面试题21. 调整数组顺序使奇数位于偶数前面
思路:使用类似快排的思想,在数组前后分别置一个指针left、right,left从前向后寻找偶数,right从后向前寻找奇数,然后交换两个位置的数字并移动两指针,直到两指针碰到一起结束。
class Solution {
public int[] exchange(int[] nums) {
int left = 0;
int right = nums.length - 1;
while (left < right) {
while (left < right && (nums[left] & 1) == 1) {
left ++;
}
while (left < right && (nums[right] & 1) == 0) {
right --;
}
swap(nums, left++, right--);
}
return nums;
}
private void swap(int[] nums, int left, int right) {
int tmp = nums[left];
nums[left] = nums[right];
nums[right] = tmp;
}
}
29. 顺时针打印矩阵
题意:面试题29. 顺时针打印矩阵
思路:双指针法。两个指针分别位于矩阵的左上角和右下角。每次打印以两个指针为边界的矩形范围(打印是要考虑特殊情况:只有一个元素、只有一行、只有一列...)。打印一圈之后缩小范围,左上角指针向右下角移动,右下角矩阵向左上角移动。
class Solution {
public int[] spiralOrder(int[][] matrix) {
if (matrix == null || matrix.length == 0) {
return new int[0];
}
int width = matrix[0].length;
int height = matrix.length;
int[] res = new int[width * height];
int leftX = 0, leftY = 0;
int rightX = height - 1, rightY = width - 1;
int start = 0;
while (start < res.length) {
start = print(matrix, leftX, leftY, rightX, rightY, res, start);
leftX ++;
leftY ++;
rightX --;
rightY --;
}
return res;
}
private int print(int[][] matrix, int leftX, int leftY, int rightX, int rightY, int[] res, int start) {
if (leftX == rightX && leftY == rightY) {
res[start ++] = matrix[leftX][leftY];
} else if (leftX == rightX) {
for (int i = leftY; i <= rightY; i ++) {
res[start ++] = matrix[leftX][i];
}
} else if (leftY == rightY) {
for (int i = leftX; i <= rightX; i ++) {
res[start ++] = matrix[i][leftY];
}
} else {
int i = leftX, j = leftY;
while (j < rightY) {
res[start ++] = matrix[i][j ++];
}
while (i < rightX) {
res[start ++] = matrix[i ++][j];
}
while (j > leftY) {
res[start ++] = matrix[i][j --];
}
while (i > leftX) {
res[start ++] = matrix[i --][j];
}
}
return start;
}
}
print函数的另外一种写法:
private int print(int[][] matrix, int leftX, int leftY, int rightX, int rightY, int[] res, int start) {
for (int i = leftY; i <= rightY; i ++) {
res[start ++] = matrix[leftX][i];
}
for (int j = leftX + 1; j <= rightX; j ++) {
res[start ++] = matrix[j][rightY];
}
if (leftX < rightX && leftY < rightY) {
for (int i = rightY - 1; i >= leftY; i --) {
res[start ++] = matrix[rightX][i];
}
for (int j = rightX - 1; j > leftX; j --) {
res[start ++] = matrix[j][leftY];
}
}
return start;
}
39. 数组中出现次数超过一次的数字
题意:面试题39. 数组中出现次数超过一半的数字
思路:假设数组中超过一半的数字为x,如果同时删除一个x和另一个非x,那么最后数组中剩下的仍然是x.
遍历数组,使用一个候选值candidate和一个计数count记录当前比较多的数字,如果遍历到的数字等于candidate,那么count加一。
如果遍历到的数字不为candidate,那么count减一,当count减到0的时候,将candidate赋值为此时遍历到的数字。
最后candidate记录的数字就是出现次数超过一半的数字。
class Solution {
public int majorityElement(int[] nums) {
int candidate = nums[0];
int count = 1;
for (int i = 1; i < nums.length; i ++) {
if (nums[i] == candidate) {
count ++;
} else {
if (count == 0) {
candidate = nums[i];
} else {
count --;
}
}
}
return candidate;
}
}
40. 最小的k个数
题意:面试题40. 最小的k个数
思路:使用大根堆。大根堆中结点个数维持在k个。遍历的过程中,如果当前元素比根结点的值小,那么将当前元素插入大根堆中,并将堆顶元素弹出。遍历完之后大根堆中保存的就是最小的k个数。
Java中大根堆可以使用优先队列来实现。
class Solution {
public int[] getLeastNumbers(int[] arr, int k) {
PriorityQueue<Integer> queue = new PriorityQueue<>((i1, i2) -> {
return i2 - i1;
});
for (int i : arr) {
if (queue.size() >= k) {
if (!queue.isEmpty() && i < queue.peek()) {
queue.poll();
queue.add(i);
}
} else {
queue.add(i);
}
}
int[] res = new int[queue.size()];
for (int i = 0; i < res.length; i ++) {
res[i] = queue.poll();
}
return res;
}
}
51. 数组中的逆序对
题意:面试题51. 数组中的逆序对
思路:归并排序。将数组划分两等份,再将两个子数组划分...一直到子数组元素个数为1,然后两两进行排序,在排序(合并)的过程中计算逆序对。
class Solution {
public int reversePairs(int[] nums) {
if (nums == null || nums.length == 0) {
return 0;
}
return reversePairs(nums, 0, nums.length - 1);
}
private int reversePairs(int[] nums, int start, int end) {
if (start == end) {
return 0;
}
int mid = (start + end) >> 1;
int left = reversePairs(nums, start, mid);
int right = reversePairs(nums, mid + 1, end);
int i = start, j = mid + 1;
int[] help = new int[end - start + 1];
int pairs = left + right;
int k = 0;
while (i <= mid && j <= end) {
if (nums[i] > nums[j]) {
pairs += (mid - i + 1);
help[k++] = nums[j++];
} else {
help[k++] = nums[i++];
}
}
int tmp = (i > mid) ? j : i;
int tmpEnd = (i > mid) ? end : mid;
while (tmp <= tmpEnd) {
help[k++] = nums[tmp++];
}
for (int t = 0; t < help.length; t ++) {
nums[start + t] = help[t];
}
return pairs;
}
}
53-I. 在排序数组中查找数字 I
题意:面试题53 - I. 在排序数组中查找数字 I
思路:二分查找。由于数组是有序的,所以可以使用二分查找,找到target的最左边的索引left和最右边的索引right,两者差值就是要求的次数。
class Solution {
public int search(int[] nums, int target) {
if (nums == null || nums.length == 0) {
return 0;
}
int right = maxIndex(nums, target);
if (right < 0) {
return 0;
}
int left = minIndex(nums, target);
return right - left + 1;
}
private int minIndex(int[] nums, int target) {
int low = 0;
int high = nums.length - 1;
int mid;
while (low < high) {
mid = low + ((high - low) >> 1);
if (nums[mid] >= target) {
high = mid;
} else {
low = mid + 1;
}
}
return nums[low] == target ? low : -1;
}
private int maxIndex(int[] nums, int target) {
int low = 0;
int high = nums.length - 1;
int mid;
while (low < high) {
mid = low + (high - low + 1) / 2;
if (nums[mid] <= target) {
low = mid;
} else {
high = mid - 1;
}
}
return nums[high] == target ? high : -1;
}
}
53-II. 0~n-1中缺失的数字
题意:面试题53 - II. 0~n-1中缺失的数字
思路:二分查找。数组整体上是递增有序的,可以采用二分查找的方式。比较数组下标和对应数字的关系。下标的值不会比数字大的情况。对于中间的数字mid,如果和下标一致,那么缺少的数字一定在mid之后。否则下标值小于对应的数字,那么缺失的值一定在mid前。
class Solution {
public int missingNumber(int[] nums) {
int low = 0;
int high = nums.length - 1;
int mid;
while (low <= high) {
mid = low + ((high - low) >> 1);
if (nums[mid] == mid) {
low = mid + 1;
} else {
high = mid - 1;
}
}
return nums[low] == low ? nums[low] + 1: nums[low] -1;
}
}
56-I. 数组中数字出现的次数
题意:面试题56 - I. 数组中数字出现的次数
思路:位运算。先将数组中所有的数字进行异或运算,最后的结果即为那两个不相同的数字异或的结果,那么这个结果其中有一位必然为1。按照该位是否为1将数组分为两组再进行异或运算,那么最后的结果就是这两个数字。
class Solution {
public int[] singleNumbers(int[] nums) {
int sum = 0;
for (int i : nums) {
sum ^= i;
}
int oneBit = sum & (-sum);
int num1 = 0;
int num2 = 0;
for (int i : nums) {
if ((i & oneBit) == 0) {
num1 ^= i;
} else {
num2 ^= i;
}
}
return new int[]{num1, num2};
}
}
56-II. 数组中数字出现的次数 II
题意:面试题56 - II. 数组中数字出现的次数 II
思路:位运算。如果能够确定一个数字的二进制位上所有1的位置,就可以唯一确定这个数字。所以只需要对数组中每个数字的每个二进制位上的1进行计数,最后判断哪一位上1的个数不是3的倍数,那么出现一次的数字该位上就为1。
class Solution {
public int singleNumber(int[] nums) {
int[] bits = new int[32];
for (int i : nums) {
for (int j = 31; j >= 0; j --) {
if (((i >> j) & 1) == 1) {
bits[j] ++;
}
}
}
int res = 0;
for (int i = 0; i < 32; i ++) {
if (bits[i] % 3 != 0) {
res |= (1 << i);
}
}
return res;
}
}
57. 和为s的两个数字
题意:面试题57. 和为s的两个数字
思路:双指针。数组递增排序,两个指针指向的数字相加,大于target则右指针左移,小于target则左指针右移。
class Solution {
public int[] twoSum(int[] nums, int target) {
int left = 0;
int right = nums.length - 1;
while (left < right) {
if (nums[left] + nums[right] == target) {
return new int[]{nums[left], nums[right]};
} else if (nums[left] + nums[right] > target) {
right--;
} else {
left++;
}
}
return new int[0];
}
}
57-II. 和为s的连续正数序列
题意:面试题57 - II. 和为s的连续正数序列
思路:滑动窗口。左右指针开始都在0位置。右指针向后移动,一直到大于或等于target时,左指针向右移动一位。重复上述步骤,直到左指针位置已经超过target的一半。
class Solution {
public int[][] findContinuousSequence(int target) {
List<int[]> list = new ArrayList<>();
int left = 0;
int right = 0;
int sum = 0;
LinkedList<Integer> queue = new LinkedList<>();
int[] tmp;
while (left < target/2 + 1) {
if (sum < target) {
right ++;
queue.add(right);
sum += right;
} else if (sum > target) {
queue.removeFirst();
left ++;
sum -= left;
} else {
tmp = queue.stream().mapToInt(i -> i).toArray();
list.add(tmp);
right ++;
queue.add(right);
sum += right;
}
}
int[][] out = new int[list.size()][];
return list.toArray(out);
}
}
59-I. 滑动窗口的最大值
题意:面试题59 - I. 滑动窗口的最大值
思路:单调栈。为了实现方便可以使用双端队列。遍历数组,当出现比尾部元素大的数字时,将尾部元素依次出队,然后将当前元素入队。保证中元素从头到尾为递减的顺序(从尾部插入)。
最大值为头部第一个元素。
在遍历的过程中,滑动窗口已经超过头部元素的索引时,要将头部元素弹出。
class Solution {
public int[] maxSlidingWindow(int[] nums, int k) {
if (nums == null || nums.length == 0) {
return new int[0];
}
Deque<Integer> queue = new LinkedList<>();
int[] res = new int[nums.length - k + 1];
for (int i = 0; i < nums.length; i ++) {
while (!queue.isEmpty() && nums[queue.getLast()] < nums[i]) {
queue.removeLast();
}
queue.add(i);
if (i >= k - 1) {
if (i > k - 1) {
while (!queue.isEmpty() && i - k >= queue.getFirst()) {
queue.removeFirst();
}
}
res[i - k + 1] = nums[queue.getFirst()];
}
}
return res;
}
}
61. 扑克牌中的顺子
题意:面试题61. 扑克牌中的顺子
思路:将数组排序。遍历数组,记录0(大小王)的个数joker,并且从第一个不为0的数字开始向后遍历,若前一个元素pre和后一个元素post相差大于1,那么就要将joker个数减一,表示使用一张大小王代替了pre和post之间的一个数字。若joker数量减少到负数了,pre和post仍然相差大于1,那么无法使这两张牌连续,直接返回false。
class Solution {
public boolean isStraight(int[] nums) {
Arrays.sort(nums);
int joker = 0;
int index = 0;
for (; index < nums.length; index ++) {
if (nums[index] == 0) {
joker ++;
} else {
break;
}
}
for (int i = index + 1; i < nums.length; i ++) {
if (nums[i] != nums[i - 1] + 1) {
while (joker >= 0 && nums[i] != nums[i-1] + 1) {
joker --;
nums[i - 1] ++;
}
if (joker < 0) {
return false;
}
}
}
return true;
}
}
66. 构建乘积数组
题意:面试题66. 构建乘积数组
思路:分别构建前缀和后缀乘积数组。然后两部分的乘积就是最终结果。
class Solution {
public int[] constructArr(int[] a) {
if (a == null || a.length == 0) {
return new int[0];
}
int[] pre = new int[a.length];
pre[0] = 1;
int[] post = new int[a.length];
post[a.length - 1] = 1;
for (int i = 1; i < a.length; i ++) {
pre[i] = pre[i - 1] * a[i - 1];
}
for (int i = a.length - 2; i >= 0; i --) {
post[i] = post[i + 1] * a[i + 1];
}
int[] res = new int[a.length];
for (int i = 0; i < a.length; i ++) {
res[i] = pre[i] * post[i];
}
return res;
}
}