Leetcode学习之数组

本文详细介绍了数组的基础知识,包括内存存储方式、数组操作以及二维数组的特点。重点讲解了二分法的原理与实现,包括在LeetCode中的应用题目。此外,文章还探讨了移除数组元素的双指针法,以及利用滑动窗口解决长度最小子数组问题。最后,展示了如何通过模拟行为解决螺旋矩阵问题,并总结了数组相关面试题的解题技巧。

数组

1. 数组理论基础

数组是非常基础的数据结构,在面试中,考察数组的题目一般在思维上都不难,主要是考察对代码的掌控能力

也就是说,想法很简单,但实现起来 可能就不是那么回事了。

首先要知道数组在内存中的存储方式,这样才能真正理解数组相关的面试题

数组是存放在连续内存空间上的相同类型数据的集合。

数组可以方便的通过下标索引的方式获取到下标下对应的数据。

举一个字符数组的例子,如图所示:
在这里插入图片描述
需要两点注意的是

  • 数组下标都是从0开始的。
  • 数组内存空间的地址是连续的

正是因为数组的在内存空间的地址是连续的,所以我们在删除或者增添元素的时候,就难免要移动其他元素的地址。

例如删除下标为3的元素,需要对下标为3的元素后面的所有元素都要做移动操作,如图所示:
在这里插入图片描述
而且大家如果使用C++的话,要注意vector 和 array的区别,vector的底层实现是array,严格来讲vector是容器,不是数组。

数组的元素是不能删的,只能覆盖。

那么二维数组直接上图,大家应该就知道怎么回事了

在这里插入图片描述
那么二维数组在内存的空间地址是连续的么?

不同编程语言的内存管理是不一样的,以C++为例,在C++中二维数组是连续分布的。

我们来做一个实验,C++测试代码如下:

void test_arr() {
    int array[2][3] = {
		{0, 1, 2},
		{3, 4, 5}
    };
    cout << &array[0][0] << " " << &array[0][1] << " " << &array[0][2] << endl;
    cout << &array[1][0] << " " << &array[1][1] << " " << &array[1][2] << endl;
}

int main() {
    test_arr();
}

测试地址为

0x7ffee4065820 0x7ffee4065824 0x7ffee4065828
0x7ffee406582c 0x7ffee4065830 0x7ffee4065834

注意地址为16进制,可以看出二维数组地址是连续一条线的。

一些同学可能看不懂内存地址,我就简单介绍一下, 0x7ffee4065820 与 0x7ffee4065824 差了一个4,就是4个字节,因为这是一个int型的数组,所以两个相邻数组元素地址差4个字节。

0x7ffee4065828 与 0x7ffee406582c 也是差了4个字节,在16进制里8 + 4 = c,c就是12。

如图:
在这里插入图片描述
所以可以看出在C++中二维数组在地址空间上是连续的。

2. 二分法

2.1 二分法简介

给定一个 n 个元素有序的(升序)整型数组 nums 和一个目标值 target ,写一个函数搜索 nums 中的 target,如果目标值存在返回下标,否则返回 -1。

运行时间为O(longn)。

2.2 二分法实现

二分法查找中需要注意右侧right值:

  • r i g h t = l e n − 1 right = len - 1 right=len1
  • r i g h t = l e n right = len right=len

第一种方法:
在数组:1,2,3,4,7,9,10中查找元素2,如图所示:
在这里插入图片描述
代码:

// 版本一
class Solution {
public:
    int search(vector<int>& nums, int target) {
        int left = 0;
        int right = nums.size() - 1; // 定义target在左闭右闭的区间里,[left, right]
        while (left <= right) { // 当left==right,区间[left, right]依然有效,所以用 <=
            int middle = left + ((right - left) / 2);// 防止溢出 等同于(left + right)/2
            if (nums[middle] > target) {
                right = middle - 1; // target 在左区间,所以[left, middle - 1]
            } else if (nums[middle] < target) {
                left = middle + 1; // target 在右区间,所以[middle + 1, right]
            } else { // nums[middle] == target
                return middle; // 数组中找到目标值,直接返回下标
            }
        }
        // 未找到目标值
        return -1;
    }
};

第二种方案:
在这里插入图片描述
在数组:1,2,3,4,7,9,10中查找元素2,如图所示:(注意和方法一的区别)

代码:

// 版本二
class Solution {
public:
    int search(vector<int>& nums, int target) {
        int left = 0;
        int right = nums.size(); // 定义target在左闭右开的区间里,即:[left, right)
        while (left < right) { // 因为left == right的时候,在[left, right)是无效的空间,所以使用 <
            int middle = left + ((right - left) >> 1);
            if (nums[middle] > target) {
                right = middle; // target 在左区间,在[left, middle)中
            } else if (nums[middle] < target) {
                left = middle + 1; // target 在右区间,在[middle + 1, right)中
            } else { // nums[middle] == target
                return middle; // 数组中找到目标值,直接返回下标
            }
        }
        // 未找到目标值
        return -1;
    }
};

个人注意

2.3 Leetcode相关题目

704 二分法查找

代码见上

35.搜索插入位置(opens new window)

class Solution{
public:
	int searchInsert(vector<int>& nums, int target){
		int left = 0, right = nums.size()-1;
		// 与正常二分法相同
		while (left <= right)
		{
			int mid = left + (right - left) / 2;

			if (target == nums[mid])
				return mid;
			else if (target > nums[mid])
				left = mid + 1;
			else 
				right = mid - 1;
		}
		return right;
		// 二分法区间区分趋近于左边,如果未找到相同的值,
		// 那么最后一次while循环中,left和right一定在该值的左边,left+1后循环结束,right即为当前数字插入位置。
	}
};

34.在排序数组中查找元素的第一个和最后一个位置

class Solution{
public:
	// 找到左边界,注意target == nums[mid]情况改动
	int left_bound(vector<int> nums, int target){
		int left = 0, right = nums.size() - 1;
		while (left <= right){
			int mid = left + (right - left) / 2;
			if (target == nums[mid])
				right = mid - 1;   # 使右指针向左移动,继续查找结果
			else if (target > nums[mid])
				left = mid + 1;
			else 
				right = mid - 1;
		}
		//判断边界条件
		if (left >= nums.size() || target != nums[left])
			return -1;
		return left;
	}
	// 找到右边界,注意target == nums[mid]情况改动
	int left_bound(vector<int> nums, int target){
		int left = 0, right = nums.size() - 1;
		while (left <= right){
			int mid = left + (right - left) / 2;
			if (target == nums[mid])
				left = mid + 1;   // 使左指针向右移动,继续查找结果
			else if (target > nums[mid])
				left = mid + 1;
			else 
				right = mid - 1;
		}
	}	
	//判断边界条件
	if (right < 0 || target != nums[right])
		return -1;
	return right;
	}
	
	vector<int> searchRange(vector<int> nums, int target){
		int left = left_bound(nums, target);
		if (left == -1) return {-1, -1};
		
		int right = right_bound(nums, target);
		return {left, right};
	}
};

69.x 的平方根

class Soltion{
public:
	int Mysqrt(int x){
		int left = 0, right = x;
		while (left <= right){
			long mid = left + (right - left) / 2; // 定义长整型,防止乘法时数字溢出
			if (x == mid*mid)
				return mid;
			else if (x > mid*mid)
				left = mid + 1;
			else
				right = mid - 1;
		}
		return right;
	}
};

367.有效的完全平方数

class Solution{
public:
	bool isPerfectSquare(int num){
		int left = 0, right = num;

		while (left <= right)
		{
			long mid = left + (right - left) / 2;
			if (num == mid*mid)
				return true;
			else if (num > mid*mid)
				left = mid + 1;
			else
				right = mid - 1;
		}
		return false;
	}
};

3. 移除元素

给你一个数组 nums 和一个值 val,你需要 原地 移除所有数值等于 val 的元素,并返回移除后数组的新长度。

不要使用额外的数组空间,你必须仅使用 O(1) 额外空间并原地修改输入数组。

元素的顺序可以改变。你不需要考虑数组中超出新长度后面的元素。

示例 1: 给定 nums = [3,2,2,3], val = 3, 函数应该返回新的长度 2, 并且 nums 中的前两个元素均为 2。 你不需要考虑数组中超出新长度后面的元素。

示例 2: 给定 nums = [0,1,2,2,3,0,4,2], val = 2, 函数应该返回新的长度 5, 并且 nums 中的前五个元素为 0, 1, 3, 0, 4。

你不需要考虑数组中超出新长度后面的元素

3.1 思路:

有的同学可能说了,多余的元素,删掉不就得了。

要知道数组的元素在内存地址中是连续的,不能单独删除数组中的某个元素,只能覆盖。

数组的基础知识可以看这里程序员算法面试中,必须掌握的数组理论知识 (opens new window)。

暴力解法(不想多解释,看看热闹就完了)

这个题目暴力的解法就是两层for循环,一个for循环遍历数组元素 ,第二个for循环更新数组。

删除过程如下:
在这里插入图片描述

请添加图片描述

  • 时间复杂度: O ( n 2 ) O(n^2) O(n2) 劝退
  • 空间复杂度: O ( 1 ) O(1) O(1)

双指针法

双指针法(快慢指针法): 通过一个快指针和慢指针在一个for循环下完成两个for循环的工作。
请添加图片描述

双指针法(快慢指针法)在数组和链表的操作中是非常常见的,很多考察数组、链表、字符串等操作的面试题,都使用双指针法。

// 时间复杂度:O(n)
// 空间复杂度:O(1)
class Solution {
public:
    int removeElement(vector<int>& nums, int val) {
        int slowIndex = 0;
        for (int fastIndex = 0; fastIndex < nums.size(); fastIndex++) {
            if (val != nums[fastIndex]) {
                nums[slowIndex++] = nums[fastIndex];
            }
        }
        return slowIndex;
    }
};

注意这些实现方法并没有改变元素的相对位置!

  • 时间复杂度: O ( n ) O(n) O(n)
  • 空间复杂度: O ( 1 ) O(1) O(1)

3.2 相关题目

27移除元素

class Solution{
public:
	int removeElement(vector<int>& nums, int val){
		int slow = 0;

		for (int fast = 0; fast < nums.size(); ++fast){
			if (nums[fast] != val){
				nums[slow++] = nums[fast];
			}
		}
		return slow;
	}
};

26删除排序数组中的重复项

class Solution{
public:
	int removeDuplicates(vector<int>& nums, int target){
		if (nums.size() <= 0) return 0;
		int slow = 1;
		for (int fast = 1; fast < nums.size(); ++fast){
			if (nums[fast-1] != nums[fast])
				nums[slow++] = nums[fast];
			++fast;
		}
		return slow;
	}
};

283 移动零

class Solution{
public:
	void moveZero(vector<int>& nums){
		int slow = 0;
		for (int fast = 0; fast < nums.size(); ++fast){
			if (nums[fast] != 0)
				swap(nums[slow++], nums[fast]);
			++fast;
		}
	}
};
  1. 比较含退格的字符串
class Solution{
public:
	bool backspaceCompare(string s, string t){
		int l1 = s.length() - 1, skip1 = 0; // 从最后开始考虑
		int l2 = t.length() - 1, skip2 = 0;
		
		//深刻理解||关系,当两者去掉#后的长度不同时,我们需要继续进行循环比较,否则,存在相同长度内字母相同,但不同长度内,字母不同现象。
		while (l1 >= 0 || l2 >= 0){
			// s找到有意义的数
			while (l1 >= 0){
				if (s[l1] == '#'){
					++skip1, --l1;
				}
				else if (skip1 > 0){
					--skip1, --l1;
				}
				else
					break;
			} 
			// t找到有意义的数
			while (l2 >= 0){
				if (s[l2] == '#'){
					++skip2, --l2;
				}
				else if (skip2 > 0){
					--skip2, --l2;
				}
				else
					break;
			}
			if (l1 >= 0 && l2 >= 0){
				if (s[l1] != t[l2])
					return false;
			}
			else{
				if (l1 >= 0 || l2 >= 0) // 保证两个数同时满足
					return false;
			}
			
			--l1;
			--l2;
		}
		return true;
	}
};

4 有序数组的平方

给你一个按 非递减顺序 排序的整数数组 nums,返回 每个数字的平方 组成的新数组,要求也按 非递减顺序 排序。

示例 1: 输入:nums = [-4,-1,0,3,10] 输出:[0,1,9,16,100] 解释:平方后,数组变为 [16,1,0,9,100],排序后,数组变为 [0,1,9,16,100]

示例 2: 输入:nums = [-7,-3,2,3,11] 输出:[4,9,9,49,121]

4.1 思路

暴力解法

最直观的相反,莫过于:每个数平方之后,排个序,美滋滋,代码如下:

class Solution{
public:
	vector<int> sortedSquares(vector<int>& nums) {
		for (int i = 0, i < nums.size(); ++i){
			nums[i] *= nums[i];
		}
		sort(nums.begin(), nums.end()); //快速排序
		return nums;
	}
};
  • 时间复杂度 O(n + nlongn)
  • 空间复杂度 O(1)
  1. 有序数组的平方
class Solution{
public:
	vector<int> sortSquares(vector<int>& nums){
		int left = 0, right = nums.size() - 1;
		vector<int> p(nums.size());  // 预留vector大小
		int index = nums.size() - 1;
		
		while (left <= right){
			if (abs(nums[left]) < abs(nums[right])){
				p[index--] = nums[right]*nums[right];
				--right;
			}
			else{
				p[index--] = nums[left]*nums[left];
				++left;
			}
		}
		return p;
	}
};
  • 时间复杂度 O(n)
  • 空间复杂度 O(n)

5 ★长度最小的子数组(滑动窗口解法)

给定一个含有 n 个正整数的数组和一个正整数 s ,找出该数组中满足其和 ≥ s 的长度最小的 连续 子数组,并返回其长度。如果不存在符合条件的子数组,返回 0。

示例:

输入:s = 7, nums = [2,3,1,2,4,3] 输出:2 解释:子数组 [4,3] 是该条件下的长度最小的子数组。

5.1 思路

暴力解法
这道题目暴力解法当然是 两个for循环,然后不断的寻找符合条件的子序列,时间复杂度很明显是O(n^2) 。

代码如下:(不建议看)

class Solution {
public:
    int minSubArrayLen(int s, vector<int>& nums) {
        int result = INT32_MAX; // 最终的结果
        int sum = 0; // 子序列的数值之和
        int subLength = 0; // 子序列的长度
        for (int i = 0; i < nums.size(); i++) { // 设置子序列起点为i
            sum = 0;
            for (int j = i; j < nums.size(); j++) { // 设置子序列终止位置为j
                sum += nums[j];
                if (sum >= s) { // 一旦发现子序列和超过了s,更新result
                    subLength = j - i + 1; // 取子序列的长度
                    result = result < subLength ? result : subLength;
                    break; // 因为我们是找符合条件最短的子序列,所以一旦符合条件就break
                }
            }
        }
        // 如果result没有被赋值的话,就返回0,说明没有符合条件的子序列
        return result == INT32_MAX ? 0 : result;
    }
};

时间复杂度: O ( n 2 ) O(n^2) O(n2) 空间复杂度: O ( 1 ) O(1) O(1)

★重点
滑动窗口

所谓滑动窗口,就是不断的调节子序列的起始位置和终止位置,从而得出我们要想的结果。

这里还是以题目中的示例来举例,s=7, 数组是 2,3,1,2,4,3,来看一下查找的过程:

在这里插入图片描述
最后找到 4,3 是最短距离。

其实从动画中可以发现滑动窗口也可以理解为双指针法的一种!只不过这种解法更像是一个窗口的移动,所以叫做滑动窗口更适合一些。

在本题中实现滑动窗口,主要确定如下三点:

窗口内是什么?
如何移动窗口的起始位置?
如何移动窗口的结束位置?

窗口就是 满足其和 ≥ s 的长度最小的 连续 子数组。

窗口的起始位置如何移动:如果当前窗口的值大于s了,窗口就要向前移动了(也就是该缩小了)。

窗口的结束位置如何移动:窗口的结束位置就是遍历数组的指针,窗口的起始位置设置为数组的起始位置就可以了。

解题的关键在于 窗口的起始位置如何移动,如图所示:
在这里插入图片描述
可以发现滑动窗口的精妙之处在于根据当前子序列和大小的情况,不断调节子序列的起始位置。从而将O(n^2)的暴力解法降为O(n)。

class Solution{
public:
	int minSubArrayLen(int target, vector<int>& nums){
		int left = 0, right = 0; // 限定左右窗口
		int sum = 0; //总和
		int subLength = 0; //长度
		int  result = INT_MAX; //最优结果

		while (right <= nums.size()){
			sum += nums[right];
			// 判断是否满足基础条件,如果满足就收缩窗口
			while (sum >= target){
				subLength = right - left + 1;
				result = result > subLength? subLength: result;
					
				sum -= nums[left];
				++left;
			}
			++right;
		}
		return result == INT_MAX? 0: result;
	}	
};

时间复杂度: O ( n ) O(n) O(n)
空间复杂度: O ( 1 ) O(1) O(1)

一些录友会疑惑为什么时间复杂度是 O ( n ) O(n) O(n)

不要以为for里放一个while就以为是 O ( n 2 ) O(n^2) O(n2)啊, 主要是看每一个元素被操作的次数,每个元素在滑动窗后进来操作一次,出去操作一次,每个元素都是被被操作两次,所以时间复杂度是2 * n 也就是 O ( n ) O(n) O(n)

5.2 相关题目

209 长度最小的子数组
代码如上

904 水果成篮

class Solution{
public:
	int totalFruit(vector<int>& fruits){
		unordered_map<int, int> basket;
		int left = 0, right = 0;
		int result = 0, subLength = 0;
		
		while (right < fruits.size()){
			basket[fruits[right]]++;
			++right;
			
			while (basket.size() > 2){
				basket[fruits[left]]--;
				if (basket[fruits[left]] == 0) basket.erase(fruits[left]);
				++left;
			}
			subLength = right - left;
			result = result > subLength? result: subLength;
		}
		return result;
	}
};	

★滑动窗口框架

void slidingWindow(string s, string t){
	unordered_map<char, int> need, window;
	for (char c: t) need[c]++;
	
	int left = 0, right = 0;
	int valid = 0;
	while (right < s.size()){
		// c是移入窗口的字符
		char c = s[right];
		// 右移窗口
		right++;
		// 进行窗口内数据的一系列更新
		...
		
		// debug 输出的位置
		cout << "window: " << "[" << left << ", " << right << endl;

		//判断左侧矿口是否要收缩
		while (window needs shrink){
			// d是将移出窗口的字符
			char d = s[left];
			// 左移窗口
			left++;
			//进行窗口内数据的一系列更新
			...
		}
	}
}

76 最小覆盖子串

class Solution{
public:
	string minWindow(string s, string t){
		unordered_map<char, int> need, window;
		for (char c : t) need[c]++;
		
		int left = 0, right = 0;
		int valid = 0;
		//记录最小覆盖子串的起始索引及长度
		int start = 0, len = INT_MAX;
		
		while (right < s.size()){
			char c = s[right];
			// c是将移入窗口的字符
			right++;
			// 进行窗口内数据的一系列更新
			if (need.count(c)){
				window[c]++;
				if (need[c] == window[c])
					valid++;
			}
			// 判断左侧窗口是否要收缩
			while (valid == need.size()){
				// 在这里更新最小覆盖字串
				if (right - left < len){
					start = left;
					len = right - left;
				}
				// d是将移出窗口的字符
				char d = s[left];
				// 左移窗口
				++left;
				// 进行窗口内数据的一系列更新
				if (need.count(d)){
					if (need[d] == window[d])
						valid--;
					window++;
				}
			}
		}
		// 返回最小覆盖子串
		return len == INT_MAX? "" : s.substr(start, len);
	}
};

567 字符串的排列

class Solution {
public:
    bool checkInclusion(string s1, string s2) {
        unordered_map<char, int> need, window;
        for (char c: s1) need[c]++;

        int left = 0, right = 0;
        int valid = 0;

        while (right < s2.size()){
            char c = s2[right];
            right++;

            if (need.count(c)){
                window[c]++;
                if (window[c] == need[c])
                    valid++;
            }

            while (right - left >= s1.size()){
                if (valid == need.size()){
                    return true;
                }

                char d = s2[left];
                ++left;
                if (need.count(d)){
                    if (window[d] == need[d])
                        valid--;
                    window[d]--;
                }
            }
        }
        return false;
    }
};

438 找到字符串中所有字母异位词

class Solution{
public:
	vector<int> findAnagrams(string s, string p){
		unordered_map<char, int> need, window;
		for (char c : p) need[c]++;
		int left = 0, right = 0;
		int valid = 0;
		vector<int> res;
		
		while (right < s.size()){
			char c = s[right++];
			
			if (need.count(c)){
				window[c]++;
				if (window[c] == need[c])
					valid++;	
			}
			
			while (right - left >= p.size()){
				if (valid == need.size())
					res.push_back(left);
				
				char d = s[left++];
				if (need.count(d)){
					if (need[d] == window[d])
						valid--;
					window[d]--;
				}
			}
		}
		return res;
	}
};

3 无重复字符的最长子串

class Solution{
public:
	int lengthOfLongestSubstring(string s){
		unordered_map<char, int> window;

		int left = 0, right = 0;
		int result = 0;

		while (right < s.size()){
			char c = s[right++];
			window[c]++;
			
			while (window[c] > 1){
				char d = s[left++];
				window[d]--;
			}
			result = max(result, right - left);
		}
		return result;
	}
};

6. 螺旋矩阵

给定一个正整数 n,生成一个包含 1 到 n2 所有元素,且元素按顺时针顺序螺旋排列的正方形矩阵。

示例:

输入: 3 输出: [ [ 1, 2, 3 ], [ 8, 9, 4 ], [ 7, 6, 5 ] ]
在这里插入图片描述

6.1 思路

求解本题依然是要坚持循环不变量原则。

模拟顺时针画矩阵的过程:

填充上行从左到右
填充右列从上到下
填充下行从右到左
填充左列从下到上

由外向内一圈一圈这么画下去。

这里一圈下来,我们要画每四条边,这四条边怎么画,每画一条边都要坚持一致的左闭右开,或者左开又闭的原则,这样这一圈才能按照统一的规则画下来。

那么我按照左闭右开的原则,来画一圈,大家看一下:
在这里插入图片描述
这里每一种颜色,代表一条边,我们遍历的长度,可以看出每一个拐角处的处理规则,拐角处让给新的一条边来继续画。

这也是坚持了每条边左闭右开的原则。

代码如下,已经详细注释了每一步的目的,可以看出while循环里判断的情况是很多的,代码里处理的原则也是统一的左闭右开。

代码:

class Solution {
public:
    vector<vector<int>> generateMatrix(int n) {
        vector<vector<int>> res(n, vector<int>(n, 0)); // 使用vector定义一个二维数组
        int startx = 0, starty = 0; // 定义每循环一个圈的起始位置
        int loop = n / 2; // 每个圈循环几次,例如n为奇数3,那么loop = 1 只是循环一圈,矩阵中间的值需要单独处理
        int mid = n / 2; // 矩阵中间的位置,例如:n为3, 中间的位置就是(1,1),n为5,中间位置为(2, 2)
        int count = 1; // 用来给矩阵中每一个空格赋值
        int offset = 1; // 每一圈循环,需要控制每一条边遍历的长度
        int i,j;
        while (loop --) {
            i = startx;
            j = starty;

            // 下面开始的四个for就是模拟转了一圈
            // 模拟填充上行从左到右(左闭右开)
            for (j = starty; j < starty + n - offset; j++) {
                res[startx][j] = count++;
            }
            // 模拟填充右列从上到下(左闭右开)
            for (i = startx; i < startx + n - offset; i++) {
                res[i][j] = count++;
            }
            // 模拟填充下行从右到左(左闭右开)
            for (; j > starty; j--) {
                res[i][j] = count++;
            }
            // 模拟填充左列从下到上(左闭右开)
            for (; i > startx; i--) {
                res[i][j] = count++;
            }

            // 第二圈开始的时候,起始位置要各自加1, 例如:第一圈起始位置是(0, 0),第二圈起始位置是(1, 1)
            startx++;
            starty++;

            // offset 控制每一圈里每一条边遍历的长度
            offset += 2;
        }

        // 如果n为奇数的话,需要单独给矩阵最中间的位置赋值
        if (n % 2) {
            res[mid][mid] = count;
        }
        return res;
    }
};

6.2 相关题目

59 螺旋矩阵 II

class Solution {
public:
    vector<vector<int>> generateMatrix(int n) {
        vector<vector<int>> res(n, vector<int>(n));

        int left = 0, right = n - 1, top = 0, bottom = n - 1;
        int count = 1;
        while(true){
        	// 从左到右
            for (int j = left; j <= right; ++j)
                res[left][j] = count++;
            if (++top > bottom) break;
            // 从上到下
            for (int i = top; i <= bottom; ++i)
                res[i][right] = count++;
            if (--right < left) break;
            // 从右到左
            for (int j = right; j >= left; --j)
                res[bottom][j] = count++;
            if (--bottom < top) break;
            // 从下到上
            for (int i = bottom; i >= top; --i)
                res[i][left] = count++;
            if (++left > right) break;
        }
        return res;
    }
};

剑指Offer 29. 顺时针打印矩阵
在这里插入图片描述

class Solution {
public:
    vector<int> spiralOrder(vector<vector<int>>& matrix) {
        if (matrix.empty() return {};
  		// 矩阵行列大小
        int rows = matrix.size(), columns = matrix[0].size();
        vector<int> order;
        // 左,右,上,下四个边界
        int left = 0, right = columns - 1, top = 0, bottom = rows - 1;
        while (true) {
            // 从左到右
            for (int column = left; column <= right; column++) {
                order.push_back(matrix[top][column]);
            }
            if (++top > bottom) break;
            // 从上到下
            for (int row = top; row <= bottom; row++) {
                order.push_back(matrix[row][right]);
            }
            if (--right < left) break;
            // 从右到左
            for (int column = right; column >= left; column--) {
                order.push_back(matrix[bottom][column]);
            }
            if (--bottom < top) break;
            // 从下到上            
            for (int row = bottom; row > top; row--) {
                order.push_back(matrix[row][left]);
            }
            if (++left > right) break;
        }
        return order;
    }
};

总结

数组是非常基础的数据结构,在面试中,考察数组的题目一般在思维上都不难,主要是考察对代码的掌控能力

也就是说,想法很简单,但实现起来 可能就不是那么回事了。

首先要知道数组在内存中的存储方式,这样才能真正理解数组相关的面试题

数组是存放在连续内存空间上的相同类型数据的集合。

数组可以方便的通过下标索引的方式获取到下标下对应的数据。

举一个字符数组的例子,如图所示

在这里插入图片描述

需要两点注意的是

数组下标都是从0开始的。
数组内存空间的地址是连续的

正是因为数组的在内存空间的地址是连续的,所以我们在删除或者增添元素的时候,就难免要移动其他元素的地址。

例如删除下标为3的元素,需要对下标为3的元素后面的所有元素都要做移动操作,如图所示:

在这里插入图片描述
而且大家如果使用C++的话,要注意vector 和 array的区别,vector的底层实现是array,严格来讲vector是容器,不是数组。

数组的元素是不能删的,只能覆盖。

那么二维数组直接上图,大家应该就知道怎么回事了

在这里插入图片描述
那么二维数组在内存的空间地址是连续的么?

我们来举一个例子,例如: int[][] rating = new int[3][4]; , 这个二维数据在内存空间可不是一个 3*4 的连续地址空间
在这里插入图片描述
所以二维数据在内存中不是 3*4 的连续地址空间,而是四条连续的地址空间组成!

数组的经典题目

  1. 二分法

这道题目呢,考察的数据的基本操作,思路很简单,但是在通过率在简单题里并不高,不要轻敌。
可以使用暴力解法,通过这道题目,如果准求更优的算法,建议试一试用二分法,来解决这道题目
暴力解法时间复杂度:O(n) 二分法时间复杂度:O(logn)
在这道题目中我们讲到了循环不变量原则,只有在循环中坚持对区间的定义,才能清楚的把握循环中的各种细节。
二分法是算法面试中的常考题,建议通过这道题目,锻炼自己手撕二分的能力。

  1. 双指针法

双指针法(快慢指针法):通过一个快指针和慢指针在一个for循环下完成两个for循环的工作。
暴力解法时间复杂度:O(n^2) 双指针时间复杂度:O(n)
这道题目迷惑了不少同学,纠结于数组中的元素为什么不能删除,主要是因为一下两点:
数组在内存中是连续的地址空间,不能释放单一元素,如果要释放,就是全释放(程序运行结束,回收内存栈空间)。
C++中vector和array的区别一定要弄清楚,vector的底层实现是array,所以vector展现出友好的一些都是因为经过包装了。
双指针法(快慢指针法\左右指针法)在数组和链表的操作中是非常常见的,很多考察数组和链表操作的面试题,都使用双指针法

  1. 滑动窗口

暴力解法时间复杂度:O(n^2) 滑动窗口时间复杂度:O(n)
本题中,主要要理解滑动窗口如何移动 窗口起始位置,达到动态更新窗口大小的,从而得出长度最小的符合条件的长度。
滑动窗口的精妙之处在于根据当前子序列和大小的情况,不断调节子序列的起始位置。从而将O(n^2)的暴力解法降为O(n)。
如果没有接触过这一类的方法,很难想到类似的解题思路,滑动窗口方法还是很巧妙的

  1. 模拟行为

模拟类的题目在数组中很常见,不涉及到什么算法,就是单纯的模拟,十分考察大家对代码的掌控能力。
在这道题目中,我们再一次介绍到了循环不变量原则,其实这也是写程序中的重要原则。
相信大家又遇到过这种情况: 感觉题目的边界调节超多,一波接着一波的判断,找边界,踩了东墙补西墙,好不容易运行通过了,代码写的十分冗余,毫无章法,其实真正解决题目的代码都是简洁的,或者有原则性的,大家可以在这道题目中体会到这一点

注:内容参考和更新于代码随想录,主要用于本人学习和使用,由于博客中的代码为本人手敲,未得到有效验证,如有问题可评论或者留言。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Fighting_1997

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值