剑指 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;
    }
}
posted @ 2020-07-19 19:55  MoonLeo2017  阅读(77)  评论(0编辑  收藏  举报