文章目录

  • 一、二维数组中的查找(leetcode 240)
  • 二、旋转数组的最小数字(leetcode 153)
  • 三、旋转数组中的查找(leetcode 33)
  • 四、数组中出现次数超过一半的数字(leetcode 169)
  • 五、把数组排成最大的数(leetcode 179)
  • 六、数组中只出现一次的数字(leetcode 136)
  • 七、排序数组中查找某一个数第一次和最后一次出现的位置(leetcode 34)
  • 八、寻找数组中的重复元素(leetcode 287)
  • 九、顺时针打印矩阵(leetcode 54)
  • 十、数组中第K大的数(leetcode 215)
  • 十一、数据流中的中位数(leetcode 295)
  • 十二、接雨水问题(leetcode 42)
  • 十三、求两个数组的交集(leetcode 350)
  • 十四、二维数组的最长递增序列(leetcode 329)
  • 十五、两个排序数组找第K大的数(leetcode 4)
  • 十六、调整数组使所有奇数位于偶数前面(剑指offer)
  • 十七、二维矩阵顺时针旋转90度(leetcode 48)
  • 十八、包含重复元素的数组全排列


一、二维数组中的查找(leetcode 240)

手撕代码之数组_二分查找


  思路:如果当前元素大于目标值,则向左移动列,如果当前元素小于目标值,则向下移动行

class Solution {
public:
    bool searchMatrix(vector<vector<int>>& matrix, int target) {
        if (matrix.empty())
            return false;
        int col_len = matrix[0].size(), row_len = matrix.size();
        int row = 0, col = col_len - 1;
        // 循环条件为行和列的作用范围
        while (row < row_len && col >= 0){
            if (matrix[row][col] == target)
                return true;
            // 如果当前元素大于目标值,则向左移动列
            else if (matrix[row][col] > target)
                col--;
            // 如果当前元素小于目标值,则向下移动行
            else
                row++;
        }
        return false;
    }
};

二、旋转数组的最小数字(leetcode 153)

手撕代码之数组_二分查找_02


  思路:二分查找,如果中间位置的值比左边元素大,则更新左边元素位置为中间位置,否则更新右边元素位置为中间位置

class Solution {
public:
    int findMin(vector<int>& nums) {
        int low = 0, high = nums.size()-1, mid = 0;
        while (nums[low] > nums[high]){
            if (high - low == 1){
                mid = high;
                break;
            }
            mid = low + (high - low) / 2;
            // 二分查找,如果中间位置的值比左边元素大,则更新左边元素位置为中间位置
            // 否则更新右边元素位置为中间位置
            if (nums[mid] > nums[low])
                low = mid;
            else
                high = mid;
        }
        return nums[mid];
    }
};

三、旋转数组中的查找(leetcode 33)

手撕代码之数组_数组_03


  思路:

手撕代码之数组_二分查找_04


对于[0,1,2,3,4,5,6,7]而言,如红色标出所示,如果nums[mid]<nums[right],即中间的数小于最右边的数,那么mid(包括mid)右边的数是有序的,如果中间的数大于最右边的数,那么mid(包括mid)左边的数是有序的。那我们每次都可以折半查找。

class Solution {
public:
    int search(vector<int>& nums, int target) {
        int left = 0, right = nums.size()-1;
        while (left <= right){
            int mid = left + (right - left) / 2;
            if (nums[mid] == target)
                return mid;
            // 如果中间数比右边数小,则右半边是有序的
            if (nums[mid] < nums[right]){
            	// 如果target在右半边范围内,则进行二分查找
                if (nums[mid] < target && target <= nums[right])
                    left = mid + 1;
                // 如果target不在右边范围,则缩小查找区间
                else
                    right = mid - 1;
            }
            // 否则左半边是有序的
            else{
            	// 如果target在左半边范围内,则进行二分查找
                if (nums[left] <= target && target < nums[mid])
                    right = mid - 1;
                // 如果target不在左边范围,则缩小查找区间
                else
                    left = mid + 1;
            }
        }
        return -1;
    }
};

四、数组中出现次数超过一半的数字(leetcode 169)

手撕代码之数组_最小堆_05


  思路:题目中要找的数字出现的次数超过数组长度的一半,也就是说它出现的次数比其他所有数字出现的次数的和还要多。因此我们可以考虑在遍历数组的时候保存两个值:一个是数组中的一个数字,一个是次数

  第一个数字作为士兵,防守阵地(cnt = 1);

  遇到相同的元素,cnt++

  如果cnt == 0时,选取新的下标值对应的元素作为防守阵地的士兵

  遇到不同的元素(即为敌人),同归于尽,cnt–

  最后留在阵地上的士兵,可能是主元素。

class Solution {
public:
    int majorityElement(vector<int>& nums) {
        int result = nums[0], count = 1, len = nums.size();
        for(int i = 1; i < len; ++i)
        {
            if(nums[i] == result)
                count++;
            else if(count == 0)
            {
                result = nums[i];
                count = 1;
            }
            else
                count --;
        }
        return result;
    }
};

五、把数组排成最大的数(leetcode 179)

手撕代码之数组_数组_06


  思路:重新定义排序规则,两个数字m和n能够拼接成mn和nm,如果mn大于nm,则m应该排在n前面;如果mn小于nm,则m应该排在n后面

class Solution {
public:
    string largestNumber(vector<int>& nums) {
        vector<string> vec_str;
        for (auto num : nums)
            vec_str.push_back(to_string(num));
        sort(vec_str.begin(), vec_str.end(), [](const string & str1, const string & str2)->bool{
            return str1 + str2 > str2 + str1;
        });
        string res;
        for (auto str : vec_str)
            res.append(str);
        if (res[0] == '0')
            return "0";
        return res;
    }
};

六、数组中只出现一次的数字(leetcode 136)

手撕代码之数组_最小堆_07


  思路:利用异或的基本性质(相同为0,相异为1,与0异或等于本身),以及交换律和结合律

class Solution {
public:
    int singleNumber(vector<int>& nums) {
        int res = nums[0], sz = nums.size();
        for (int i = 1; i < sz; ++i){
            res ^= nums[i];
        }
        return res;
    }
};

七、排序数组中查找某一个数第一次和最后一次出现的位置(leetcode 34)

手撕代码之数组_最小堆_08


  思路:二分查找的变式,注意边界条件

class Solution {
public:
    vector<int> searchRange(vector<int>& nums, int target) {
        int low = lower_bound(nums, target);
        int high = upper_bound(nums, target);
        if (nums.empty() || nums[low] != target)
            return vector<int>{-1, -1};
        else
            return vector<int>{low, high};
    }
    // 查找第一次出现的位置
    int lower_bound(vector<int>& nums, int target){
        int low = 0, high = nums.size() - 1;
        while (low < high){
            int mid = low + (high - low) / 2;
            if (nums[mid] >= target)
                high = mid;
            else
                low = mid + 1;
        }
        return low;
    }
    // 查找最后一次出现的位置
    int upper_bound(vector<int>& nums, int target){
        int low = 0, high = nums.size() - 1;
        while (low <= high){
            int mid = low + (high - low) / 2;
            if (nums[mid] <= target)
                low = mid + 1;
            else
                high = mid - 1;
        }
        return high;
    }
};

八、寻找数组中的重复元素(leetcode 287)

手撕代码之数组_数组_09


  思路:用哈希表存储每一个元素出现的次数。

class Solution {
public:
    int findDuplicate(vector<int>& nums) {
        int res;
        unordered_map<int, int> ump;
        for (auto num : nums){
            ump[num]++;
            if (ump[num] > 1){
                res = num;
                break;
            }
        }
        return res;
    }
};

九、顺时针打印矩阵(leetcode 54)

手撕代码之数组_数组_10


  思路:用四个变量记录下边界,然后分析打印每一步的前提条件

手撕代码之数组_数组_11

class Solution {
public:
    vector<int> spiralOrder(vector<vector<int>>& matrix) {
        vector<int> res;
        if (matrix.empty())
            return res;
        // 用四个变量记录下边界
        int top = 0, btm = matrix.size()-1, left = 0, right = matrix[0].size()-1;
        while (left <= right && top <= btm){
            // 打印第一步不需要条件
            for (int j = left; j <= right; ++j){
                res.push_back(matrix[top][j]);
            }
            // 打印第二步的前提条件是(top<btm)
            if (top < btm){
                for (int i = top+1; i <= btm; ++i){
                    res.push_back(matrix[i][right]);
                }    
            }
            // 打印第三步的前提条件是(top<btm && left<right)
            if (top < btm && left < right){
                for (int j = right-1; j >= left; --j){
                    res.push_back(matrix[btm][j]);
                }                    
            }
            // 打印第四步的前提条件是(top+1<btm&&left<right)
            if (left < right && top+1 < btm){
                for (int i = btm-1; i >= top+1; --i){
                    res.push_back(matrix[i][left]);
                }
            }
            ++top, ++left, --btm, --right;
        }
        return res;
    }
};

十、数组中第K大的数(leetcode 215)

手撕代码之数组_最小堆_12


  思路:基于快排的思想,比较k和mid的位置关系,每次只需要利用一个区间即可。

手撕代码之数组_最小堆_13

class Solution {
public:
    int findKthLargest(vector<int>& nums, int k) {
        quick_sort(nums, 0, nums.size()-1, k);
        return nums[nums.size() - k];
    }
    void quick_sort(vector<int>& nums, int low, int high, int k){
        if (low < high){
            int mid = partition(nums, low, high);
            // 比较k和mid的位置关系,每次只需要利用一个区间即可
            if (mid > nums.size() - k)
                quick_sort(nums, 0, mid - 1, k);
            else if (mid < nums.size() - k)
                quick_sort(nums, mid + 1, high, k);
            else
                return;
        }
    }
    // 根据枢纽元划分左右区间
    int partition(vector<int>& nums, int low, int high){
        int pivot = nums[low], mid = low;
        for (int i = low + 1; i <= high; ++i)
            if (nums[i] <= pivot)
                swap(nums[i], nums[++mid]);
        swap(nums[low], nums[mid]);
        return mid;
    }
};

十一、数据流中的中位数(leetcode 295)

手撕代码之数组_最小堆_14


  思路:基于最大堆和最小堆来实现,新元素先加入到最大堆,然后将最大堆的堆顶元素加入最小堆中并弹出,这样保证了最小堆的堆顶元素永远大于最大堆的堆顶元素。如果最小堆的元素个数大于最大堆的元素个数,则继续弹出和加入操作,保证最大堆的元素个数始终与最小堆的元素个数相差不超过1

手撕代码之数组_数组_15

class MedianFinder {
public:
    priority_queue<int, vector<int>, less<int>> large; // 定义最大堆
    priority_queue<int, vector<int>, greater<int>> small; // 定义最小堆
    MedianFinder() { }
    
    void addNum(int num) {
        // 保证最小堆的堆顶元素永远大于最大堆的堆顶元素
        large.push(num);
        small.push(large.top());
        large.pop();
        if (small.size() > large.size()){
            // 保证最大堆的堆顶元素永远小于最小堆的堆顶元素
            large.push(small.top());
            small.pop(); 
        }
    }
    
    double findMedian() {
        return large.size() > small.size() ? 
        		large.top() : 0.5 * (small.top() + large.top());
    }
};

十二、接雨水问题(leetcode 42)

手撕代码之数组_最小堆_16


  思路:保存一个左指针left,右指针right,以及左指针遍历的最大值maxleft以及右指针遍历的最大值maxright。如果当前高度小于maxleft,说明有坑,需要补全,对于right也是一样,但是要保持height[left] < height[right],目的是找到最高点。

class Solution {
public:
    int trap(vector<int>& height) {
        if (height.size() == 0) return 0;
        int left = 0, right = height.size() - 1, maxleft = 0, maxright = 0, res = 0;
        while (left < right) {
            if (height[left] < height[right]){
                maxleft = max(maxleft, height[left]);
                res += maxleft - height[left];
                left++;
            } else {
                maxright = max(maxright, height[right]);
                res += maxright - height[right];
                right--;                
            }
        }
        return res;
    }
};

十三、求两个数组的交集(leetcode 350)

手撕代码之数组_二分查找_17


  思路:用哈希表记录下较长的数组中的每个元素及其出现的次数,遍历较短长度的数组,如果能找到则递减计数

class Solution {
public:
    vector<int> intersect(vector<int>& nums1, vector<int>& nums2) {
        vector<int> res;
        if (nums1.size() < nums2.size()) 
            swap(nums1, nums2);
        unordered_map<int, int> mp;
        // 用哈希表记录下较长的数组中的每个元素及其出现的次数
        for (int i = 0; i < nums1.size(); ++i)
            mp[nums1[i]]++;
        for (auto num : nums2){ 
            if (mp.find(num) != mp.end() && mp[num] > 0){
                res.push_back(num);
                mp[num]--;
            }
        }
        return res;
    }
};

十四、二维数组的最长递增序列(leetcode 329)

手撕代码之数组_数组_18


  思路:深度优先搜索+记忆化搜索,具体方法如下:从起点开始遍历矩阵,递归寻找其最长递增路径。定义辅助数组record,用于记录已经搜索过的矩阵元素的数据,如record[x][y]存储了从坐标(x, y)出发的最长递增路径长度。之后,进行深度优先搜索。逐一检查某个元素的四个方向,并继续从所有可能出现最长路径的方向上进行搜索。 当遇到record[x][y]已算出的情况,直接返回record[x][y],减少运算量。

class Solution {
public:
    // record[x][y]存储了从坐标(x, y)出发的最长递增路径长度。
    int dfs(vector<vector<int>>& matrix, vector<vector<int>>& record, int x, int y, int lastval) {
        if (x < 0 || y < 0 || x >= matrix.size() || y >= matrix[0].size()) return 0;
        if (matrix[x][y] > lastval) {
            // 路线已算出,直接返回结果
            if (record[x][y] != 0) return record[x][y];
            // 向四周搜索
            int left = dfs(matrix, record, x + 1, y, matrix[x][y]) + 1;
            int right = dfs(matrix, record, x - 1, y, matrix[x][y]) + 1;
            int top = dfs(matrix, record, x, y + 1, matrix[x][y]) + 1;
            int bottom = dfs(matrix, record, x, y - 1, matrix[x][y]) + 1;
            record[x][y] = max(left, max(right, max(top, bottom)));
            return record[x][y];
        }
        return 0;
    }
    int longestIncreasingPath(vector<vector<int>>& matrix) {
        if (matrix.size() == 0) return 0;
        int res = 0;
        vector<vector<int>> record(matrix.size(), vector<int>(matrix[0].size(), 0));
        for (int i = 0; i < matrix.size(); ++i) {
            for (int j = 0; j < matrix[0].size(); ++j) {
                res = max(res, dfs(matrix, record, i, j, -1));
            }
        }
        return res;
    }
};

十五、两个排序数组找第K大的数(leetcode 4)

手撕代码之数组_数组_19


  思路:比较这两个数组的第 K/2 小的数字的大小,如果第一个数组的第 K/2 个数字较小的话,那么说明我们要找的数字肯定不在 nums1 中的前 K/2 个数字,所以我们可以将其淘汰,将 nums1 的起始位置向后移动 K/2 个,并且此时的K也自减去 K/2,调用递归。

手撕代码之数组_数组_20

class Solution {
public:
    double findMedianSortedArrays(vector<int>& nums1, vector<int>& nums2) {
        int m = nums1.size(), n = nums2.size();
        return (findKth(nums1, nums2, (m + n + 1) / 2) + findKth(nums1, nums2, (m + n + 2) / 2)) / 2.0;
    }
    int findKth(vector<int> nums1, vector<int> nums2, int k) {
        if (nums1.empty()) return nums2[k - 1];
        if (nums2.empty()) return nums1[k - 1];
        if (k == 1) return min(nums1[0], nums2[0]);
        int i = min((int)nums1.size(), k / 2), j = min((int)nums2.size(), k / 2);
        if (nums1[i - 1] > nums2[j - 1]) {
            return findKth(nums1, vector<int>(nums2.begin() + j, nums2.end()), k - j);
        } else {
            return findKth(vector<int>(nums1.begin() + i, nums1.end()), nums2, k - i);
        }
        return 0;
    }
};

十六、调整数组使所有奇数位于偶数前面(剑指offer)

手撕代码之数组_二分查找_21


  思路:解法一是利用冒泡排序的方法,遇到前偶后奇就交换;解法二是空间换时间的做法,先摘取再合并

// 解法一
class Solution {
public:
    void reOrderArray(vector<int> &array) {
        // 类似冒泡算法,前偶后奇数就交换
        for (int i = 0; i < array.size(); ++i) {
            for (int j = array.size()-1; j > i; --j) {
                if (array[j] % 2 == 1 && array[j-1] % 2 == 0)
                    swap(array[j], array[j-1]);
            }
        }
    }
};

// 解法二
class Solution {
public:
    void reOrderArray(vector<int> &array) {
        vector<int> res;
        for(int i = 0; i < array.size(); i++)
        {
            if(array[i] % 2 == 1)
                res.push_back(array[i]);
        }
        for(int i = 0; i < array.size(); i++)
        {
            if(array[i] % 2 == 0)
                res.push_back(array[i]);
        }
        array.swap(res);
    }
};

十七、二维矩阵顺时针旋转90度(leetcode 48)

手撕代码之数组_数组_22


  思路:先求转置,再把每一行反转

class Solution {
public:
    void rotate(vector<vector<int>>& matrix) {
        /*
        * 先求转置再反转每一行
        */
        int sz = matrix.size();
        for (int i = 0; i < sz; ++i){
            for (int j = i+1; j < sz; ++j)
                swap(matrix[i][j], matrix[j][i]);
            reverse(matrix[i].begin(), matrix[i].end());
        }
        return;
    }
};

十八、包含重复元素的数组全排列

#include <iostream>
#include <string>
#include <algorithm>
#include <vector>
#include <set>
using namespace std;

set<vector<int>> ans; //用set去重

void fun(vector<int> &vec, int start, int len)
{
	// 当start==arr.length-1时,说明子序列的长度为1,就不用再往下划分子序列了
	if (start == len)
		ans.insert(vec);
	for (int i = start; i < len; i++)
	{
		swap(vec[start], vec[i]);
		fun(vec, start + 1, len);
		swap(vec[start], vec[i]);
	}
}

vector<vector<int>> Permutation(vector<int> vec) {
	vector<vector<int>> vecout;
	if (!vec.empty())
	{
		fun(vec, 0, vec.size());
		for (auto tmp : ans)
			vecout.push_back(tmp);
	}
	return vecout;
}

int main() {
	vector<vector<int>> res = Permutation({ 1, 2, 3 });
	for (int i = 0; i < res.size(); ++i) {
		for (int j = 0; j < res[0].size(); ++j) {
			cout << res[i][j] << " ";
		}
		cout << endl;
	}
	system("pause");
	return 0;
}