二维数组中的查找
题目描述:在一个n*m的二维数组中,每一行都按照从左到右递增的顺序排序,每列都按照从上到下递增的顺序排序。请完成一个高效的函数,输入这样的一个二维数组和整数,判断数组中是否含有该整数。
示例:
现有矩阵matrix如下:
[
[1, 4, 7, 11, 15],
[2, 5, 8, 12, 19],
[3, 6, 9, 16, 22],
[10, 13, 14, 17, 24],
[18, 21, 23, 26, 30]
]
给定target=5,返回true
给定target=20,返回false
方法一:暴力破解法
思路如下:依次遍历数组的 每一行和列,找到返回true,未找到则返回false。
代码:
class Solution{
public boolean findNumberIn2DArray(int[][] matrix,int target){
if(matrix == null || matrix.length==0||matrix[0].length==0){
return false;
}
int rows = matrix.length,colums = matrix[0].length;
for (int i = 0;i<rows;i++){
for (int j = 0;j<colums;j++){
if(matrix[i][j] == target){
return true;
}
}
}
return false;
}
}
时间复杂度:O(nm)
控件复杂度:O(1)
方法二:线性查找
思路如下:由于给定的二维数组具备每行从左到右递增,每列从上到下递增,每当访问到一个元素时,可以排除数组中的部分元素,从左上角开始查找,如果相等则返回true,如果当前元素大于target则移到左边一列,如果当前元素小于目标值则移到下边一行。
代码:
class Solution {
public boolean findNumberIn2DArray(int[][] matrix, int target) {
if(matrix == null||matrix.length == 0||matrix[0].length==0){
return false;
}
int rows = matrix.length,colunms = matrix[0].length;
int row = 0;
int colunm = colunms-1;
while(row < rows && colunm >= 0){
int num = matrix[row][colunm];
if(num == target){
return true;
}else if(num>target){
colunm--;
}else{
row++;
}
}
return false;
}
}
时间复杂度:O(n+m)
空间复杂度:O(1)
方法三:二分法
思路:因为所给数组皆有序,我们可以对每一行进行二分查找。随后挪至下一行,直至结束。
代码:
class Solution {
public boolean findNumberIn2DArray(int[][] matrix, int target) {
if(matrix == null || matrix.length == 0 || matrix[0].length == 0) {
return false;
}
for(int i=0;i<matrix.length;i++){
int left = 0,right = matrix[0].length-1;
while(left<=right){
int mid = left+(right-left)/2;
if(matrix[i][mid]==target){
return true;
}else if(matrix[i][mid]>target){
right = mid-1;
}else{
left = mid+1;
}
}
}
return false;
}
}
时间复杂度:因为二分复杂度为O(log2n),加上for循环,应为O(nlog2n)。
空间复杂度:O(1)
旋转数组的最小值
题目描述:把一个数组最开始的若干个元素搬到数组的末尾,我们称之为数组的旋转。输入一个递增排序的数组的一个旋转,输出旋转数组的最小元素。例如,数组 [3,4,5,1,2] 为 [1,2,3,4,5] 的一个旋转,该数组的最小值为1。
示例1:
输入:[3,4,5,1,2]
输出:1
示例2:
输入:[2,2,2,0,1]
输出:0
思路描述:包含重复元素的升序数组旋转后如下
左边界为low,右边界为high,中点为pivot
将中轴元素与右边界比较,有如下三种情况。
①:
②:
③:
代码:
class Solution {
public int minArray(int[] numbers) {
int left = 0,right = numbers.length-1;
while(left<right){
int mid = left+(right-left)/2;
if(numbers[mid]>numbers[right]){
left = mid+1;
}else if(numbers[mid]<numbers[left]){
right = mid;
}else{
right--;
}
}
return numbers[left];
}
}
最坏情况时间复杂度(元素全相同):O(n)
平均时间复杂度:O(logn)
空间复杂度:O(1)
调整数组使得奇数在前偶数在后
题目描述:输入一个整数数组,实现一个函数来调整该数组中数字的顺序,使得所有奇数位于数组的前半部分,所有偶数位于数组的后半部分。
示例:
输入:nums = [1,2,3,4]
输出:[1,3,2,4]
注:[3,1,2,4] 也是正确的答案之一。
方法一:辅助数组
思路如下::创建为原数组大小的新数组,遍历两遍数组,第一遍遍历奇数,放入新数组,第二遍遍历偶数,放入新数组。
代码:
class Soultion{
public int[] exchange(int[] nums){
int n = nums.length;
int[] res = new int[n];
int index = 0;
for (int num:nums){
if(num%2==1){
res[index++] = num;
}
}
for (int num:nums){
if (num%2==0){
res[index++] = num;
}
}
return res;
}
}
时间复杂度:O(n)
空间复杂度:O(n)
方法二:头尾双指针
思路:left指向头,right指向尾。left右移直到指向偶数,right左移直到指向奇数。交换left和right的数字,直到left>right。若left>right,则break结束。
代码:
class Solution {
public int[] exchange(int[] nums) {
int pre = 0,post = nums.length-1;
while(pre<=post){
while(pre<=post && nums[pre] % 2 == 1){
pre++;
}
while(pre<=post && nums[post] % 2 == 0){
post--;
}
if(pre>post){
break;
}
int cur = nums[post];
nums[post] = nums[pre];
nums[pre] = cur;
}
return nums;
}
}
时间复杂度
方法三:快慢指针
思路:slow和fast都从头开始遍历,fast右移,如若遇到奇数,则和slow位置数字交换,slow++,fast++。没有遇到奇数,fast++,直至末尾结束。
代码:
class Solution {
public int[] exchange(int[] nums) {
int slow = 0,fast = 0;
while(fast<nums.length){
if(nums[fast]%2 == 1){
int tmp = nums[slow];
nums[slow] = nums[fast];
nums[fast] = tmp;
slow++;
}
fast++;
}
return nums;
}
}
顺时针打印矩阵
题目描述:输入一个矩阵,按照从外向里以顺时针的顺序依次打印出每一个数字。
示例1:
输入:matrix = [[1,2,3],[4,5,6],[7,8,9]]
输出:[1,2,3,6,9,8,7,4,5]
示例2:
输入:matrix = [[1,2,3,4],[5,6,7,8],[9,10,11,12]]
输出:[1,2,3,4,8,12,11,10,9,5,6,7]
方法一:模拟矩阵打印路径
思路如下:
按照打印矩阵的路径,初始位置为左上角,初始方向向右,当超出路径或者进入到之前已经访问过的位置,就顺时针旋转进入下一个方向。
我们使用一个辅助矩阵来标记是否访问过这个位置。至于旋转的时机,我们使用一个二维数组,实现四个方向的改变,每次更新下一行和下一列的下标,具体实现请看代码。
代码:
class Solution {
public int[] spiralOrder(int[][] matrix) {
if( matrix == null || matrix.length == 0 || matrix[0].length == 0){
return new int[0];
}
int rows = matrix.length,colums = matrix[0].length;
boolean[][] vis = new boolean[rows][colums];
int total = rows*colums;
int[] n = new int[total];
int row =0,colum = 0;
int[][] directions = {{0,1},{1,0},{0,-1},{-1,0}};
int directionIndex = 0;
for(int i=0;i<total;i++){
n[i] = matrix[row][colum];
vis[row][colum] = true;
int nextRow = row+directions[directionIndex][0],nextColum = colum+directions[directionIndex][1]; //在遍历行时,nextRow的值不变,nextColum的值加1,直到边界,反之亦然。
if(nextRow<0||nextRow>=rows||nextColum<0||nextColum>=colums||vis[nextRow][nextColum]){
directionIndex = (directionIndex+1)%4; //在超过边界或是到访问过的位置,更新dir的值
}
row+=directions[directionIndex][0];
colum+=directions[directionIndex][1];
}
return n;
}
}
时间复杂度:O(mn),m,n分别为行列数。
空间复杂度:O(mn)。需要辅助矩阵。
方法二:按层模拟
思路如下:我们可以把矩阵看成很多层,首先打印最外层元素,其次打印内层元素,直到结束。
如下所示,每一层都按同样的顺序打印,就像洋葱一样一层一层剥开。
[[1, 1, 1, 1, 1, 1, 1],
[1, 2, 2, 2, 2, 2, 1],
[1, 2, 3, 3, 3, 2, 1],
[1, 2, 2, 2, 2, 2, 1],
[1, 1, 1, 1, 1, 1, 1]]
左上角位于 (\textit{top}, \textit{left})(top,left),右下角位于 (\textit{bottom}, \textit{right})(bottom,right),按照如下顺序遍历当前层的元素。
- 从左到右遍历上侧元素,依次为 (\textit{top}, \textit{left})(top,left) 到 (\textit{top}, \textit{right})(top,right)。
- 从上到下遍历右侧元素,依次为 (\textit{top} + 1, \textit{right})(top+1,right) 到 (\textit{bottom}, \textit{right})(bottom,right)。
- 如果 \textit{left} < \textit{right}left<right 且 \textit{top} < \textit{bottom}top<bottom,则从右到左遍历下侧元素,依次为 (\textit{bottom}, \textit{right} - 1)(bottom,right−1) 到 (\textit{bottom}, \textit{left} + 1)(bottom,left+1),以及从下到上遍历左侧元素,依次为 (\textit{bottom}, \textit{left})(bottom,left) 到 (\textit{top} + 1, \textit{left})(top+1,left)
遍历完当前层的元素之后,将 \textit{left}left 和 \textit{top}top 分别增加 11,将 \textit{right}right 和 \textit{bottom}bottom 分别减少 11,进入下一层继续遍历,直到遍历完所有元素为止。
代码:
class Solution {
public int[] spiralOrder(int[][] matrix) {
if(matrix == null || matrix.length == 0 || matrix[0].length == 0){
return new int[0];
}
int rows = matrix.length,colums = matrix[0].length;
int[] n = new int[rows*colums];
int index = 0;
int left = 0,right = colums-1,top = 0,bottom = rows-1;
while(left<=right && top<=bottom){
for(int colum = left;colum<=right;colum++){
n[index++] = matrix[top][colum];
}
for(int row = top+1;row<=bottom;row++){
n[index++] = matrix[row][right];
}
if(left<right&&top<bottom){
for(int colum = right-1;colum>left;colum--){
n[index++] = matrix[bottom][colum];
}
for(int row= bottom;row>top;row--){
n[index++] = matrix[row][left];
}
}
left++;
right--;
top++;
bottom--;
}
return n;
}
}
时间复杂度:O(nm),n,m为行列数,每个元素都需要访问。
空间复杂度:O(1)
数组中出现次数超过一半的数字
题目描述:数组中有一个数字出现的次数超过数组长度的一半,请找出这个数字。
你可以假设数组是非空的,并且给定的数组总是存在多数元素。
示例:
输入: [1, 2, 3, 2, 2, 2, 5, 4, 2]
输出: 2
方法一:哈希表
思路如下:我们可以使用哈希映射来保存每个元素和它出现的次数,键位元素,值为该元素出现的次数。
循环遍历数组将元素存入哈希表。之后遍历哈希表返回值最大的键。
代码:
class Solution {
public int majorityElement(int[] nums) {
Map<Integer,Integer> counts = countNums(nums);
Map.Entry<Integer,Integer> majorityElement = null;
for(Map.Entry<Integer,Integer> entry:counts.entrySet()){
if(majorityElement == null||entry.getValue()>majorityElement.getValue()){
majorityElement = entry;
}
}
return majorityElement.getKey();
}
private Map<Integer,Integer> countNums(int[] nums){
Map<Integer,Integer> counts = new HashMap<Integer,Integer>();
for(int num:nums){
if(!counts.containsKey(num)){
counts.put(num,1);
}else{
counts.put(num,counts.get(num)+1);
}
}
return counts;
}
}
时间复杂度:O(n)
空间复杂度:O(n)
方法二:排序
思路如下:如果将数组中的元素排序,无论递增或是递减,那么下标为n/2的元素(下标从0起)一定是众数。
无论数组为偶数还是奇数,因为目标元素出现的次数大于一半,所以n/2的下标位置必定为目标元素。
代码:
class Solution {
public int majorityElement(int[] nums) {
Arrays.sort(nums);
return nums[nums.length / 2];
}
}
时间复杂度:O(nlog n)
空间复杂度:O(logn)。如果使用语言自带的排序算法,需要使用 O(\log n)O(logn) 的栈空间。如果自己编写堆排序,则只需要使用 O(1)O(1) 的额外空间
方法三:分治策略
思路如下:如果数 a 是数组 nums 的众数,如果我们将 nums 分成两部分,那么 a 必定是至少一部分的众数。这样以来,我们就可以使用分治法解决这个问题:将数组分成左右两部分,分别求出左半部分的众数 a1 以及右半部分的众数 a2,随后在 a1 和 a2 中选出正确的众数。
代码:
class Solution {
public int countInRange(int[] nums,int num,int lo,int hi){
int count = 0;
for(int i=lo;i<=hi;i++){
if(nums[i]==num){
count++;
}
}
return count;
}
private int majorityElementRec(int[] nums,int lo,int hi){
if(lo == hi) {
return nums[lo];
}
int mid = (hi-lo)/2+lo;
int left = majorityElementRec(nums,lo,mid);
int right = majorityElementRec(nums,mid+1,hi);
if(left == right){
return left;
}
int leftCount = countInRange(nums,left,lo,hi);
int rightCount = countInRange(nums,right,lo,hi);
return leftCount>rightCount?left:right;
}
public int majorityElement(int[] nums) {
return majorityElementRec(nums,0,nums.length-1);
}
}
时间复杂度:O(nlog n)
空间复杂度:O(log n),虽然没有直接借助辅助空间,但是在递归过程中使用了额外的栈空间(栈帧)。
统计一个数在排序数组出现的次数
题目描述:统计一个数字在排序数组中出现的次数。
示例1:
输入: nums = [5,7,7,8,8,10], target = 8
输出: 2
示例2:
输入: nums = [5,7,7,8,8,10], target = 6
输出: 0
思路如下:首先,最简单的方法当然是遍历一遍数组,用变量记录目标元素出现的次数,但是这个方法的时间复杂度为O(n),并且没有用到数组升序的条件。
我们可以考虑使用二分法解题,要记录target出现的次数,那么就要找到target第一次出现的位置,和target最后一次出现的后一个位置。结果就是rightIndex-leftIndex+1。
代码:
class Solution {
public int search(int[] nums, int target) {
int leftIndex = binarySearch(nums,target,true);
int rightIndex = binarySearch(nums,target,false)-1;
if(leftIndex<=rightIndex&&rightIndex<nums.length&&nums[leftIndex]==target&&nums[rightIndex]==target){
return rightIndex-leftIndex+1;
}
return 0;
}
public int binarySearch(int[] nums,int target,boolean lower){
int left = 0,right = nums.length-1,ans = nums.length;
while(left<=right){
int mid = (left+right)/2;
if(nums[mid]>target||(lower&&nums[mid]>=target)){
right = mid-1;
ans = mid;
}else{
left =mid+1;
}
}
return ans;
}
}
求数组中最小的k个数
题目描述:输入整数数组 arr ,找出其中最小的 k 个数。例如,输入4、5、1、6、2、7、3、8这8个数字,则最小的4个数字是1、2、3、4。
示例1:
输入:arr = [3,2,1], k = 2
输出:[1,2] 或者 [2,1]
示例2:
输入:arr = [0,1,2,1], k = 1
输出:[0]
方法一:排序
对数组进行排序然后取出前k个数
代码:
class Solution {
public int[] getLeastNumbers(int[] arr, int k) {
int[] v = new int[k];
Arrays.sort(arr);
for(int i=0;i<k;i++){
v[i] = arr[i];
}
return v;
}
}
时间复杂度:O(nlog n)
空间复杂度:O(log n)
方法二:自建堆
我们使用一个大根堆实时维护数组的前k个值。先把k个数存在大根堆,然后从k+1个数遍历,如果比堆顶的数小,就让堆顶的数弹出,插入当前遍历到的数。最后把大根堆里的数放入数组返回。
代码:
class Solution {
public int[] getLeastNumbers(int[] arr, int k) {
int[] v = new int[k];
if(k == 0){
return v;
}
PriorityQueue<Integer> queue = new PriorityQueue<Integer>(new Comparator<Integer>(){
public int compare(Integer num1,Integer num2){
return num2-num1;
}
});
for(int i =0 ;i<k;++i){
queue.offer(arr[i]);
}
for(int i = k;i<arr.length;++i){
if(queue.peek()>arr[i]){
queue.poll();
queue.offer(arr[i]);
}
}
for(int i =0;i<k;++i){
v[i] = queue.poll();
}
return v;
}
}
时间复杂度:O(nlog k)
空间复杂度:O(k)