1.数组
数组理论基础
数组是存放在连续内存空间上的相同类型数据的集合。
数组可以⽅便的通过下标索引的⽅式获取到下标下对应的数据。
![image-20240406075504010](https://i-blog.csdnimg.cn/blog_migrate/7a8d80a73c3e79e77c92b176f65c03bd.png)
需要两点注意的是
数组下标都是从0开始的。
数组内存空间的地址是连续的
正是因为数组的在内存空间的地址是连续的,所以我们在删除或者增添元素的时候,就难免要移动其他元素的地址。
如果使⽤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();
}
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
数组套路总结
二分查找
使用二分查找的前提:数组为有序数组,同时题⽬还强调数组中⽆重复元素
⼆分查找涉及的很多的边界条件,逻辑⽐较简单,但就是写不好。例如到底是 while(left < right) 还是
while(left <= right) ,到底是 right = middle 呢,还是要 right = middle - 1 呢?
⼤家写⼆分法经常写乱,主要是因为对区间的定义没有想清楚,区间的定义就是不变量。要在⼆分查找的过程中,
保持不变量,就是在while寻找中每⼀次边界的处理都要坚持根据区间的定义来操作,这就是循环不变量规则。
写⼆分法,区间的定义⼀般为两种,左闭右闭即[left, right],或者左闭右开即[left, right)。
双指针法
双指针法(快慢指针法):通过一个快指针和慢指针在一个for循环下完成两个for循环的工作。
- 暴力解法时间复杂度:O(n^2)
- 双指针时间复杂度:O(n)
这道题目迷惑了不少同学,纠结于数组中的元素为什么不能删除,主要是因为以下两点:
- 数组在内存中是连续的地址空间,不能释放单一元素,如果要释放,就是全释放(程序运行结束,回收内存栈空间)。
- C++中vector和array的区别一定要弄清楚,vector的底层实现是array,封装后使用更友好。
双指针法(快慢指针法)在数组和链表的操作中是非常常见的,很多考察数组和链表操作的面试题,都使用双指针法。
滑动窗口
所谓滑动窗口,就是不断的调节子序列的起始位置和终止位置,从而得出我们要想的结果。
在暴力解法中,是一个for循环滑动窗口的起始位置,一个for循环为滑动窗口的终止位置,用两个for循环 完成了一个不断搜索区间的过程。
那么滑动窗口如何用一个for循环来完成这个操作呢。
首先要思考 如果用一个for循环,那么应该表示 滑动窗口的起始位置,还是终止位置。
如果只用一个for循环来表示 滑动窗口的起始位置,那么如何遍历剩下的终止位置?
此时难免再次陷入 暴力解法的怪圈。
所以 只用一个for循环,那么这个循环的索引,一定是表示 滑动窗口的终止位置。
练习
Hjk计算数组中心位置
题目描述
给你一个整数数组nums,请计算数组的中心位置,数组的中心位置是数组的一个下标,其左侧所有元素相乘的积等于右侧所有元素相的积。数组第一个元素的左侧积为1,最后一个元素的右侧积为1。如果数组有多个中心位置,应该返回最靠近左边的那一个,如果数组不存在中心位置,返回-1.
输入描述
输入只有一行,给出N个正整数用空格分隔:nums=253656
1<=nums.length<=1024
1<=nums[i]<=10
输出描述
输出:3
解释:中心位置是3
Hjk数组去重和排序
数组去重和排序
题目描述:
给定一个乱序的数组,删除所有的重复元素,使得每个元素只出现一次,并且按照出现的次数从高到低进行排序,相同出现次数按照第一次出现顺序进行先后排序。
输入描述:
一个数组stst
输出描述:
去重排序后的数组
示例 1:
输入
1,3,3,3,2,4,4,4,5
输出
3,4,1,2,5
备注
数组大小不超过100 数组元素值大小不超过100
解题思路
统计字符串信息:遍历字符串序列,统计每个字符串出现的次数以及第一次出现的位置,并将统计结果存储在一个字典中。字典的键是字符串,值是包含字符串信息(次数和第一次出现的位置)的对象。
排序:使用优先队列(堆)对字符串信息进行排序。排序的规则是首先按照出现次数从多到少排序,如果出现次数相同,则按照第一次出现的位置从小到大排序。
#include <iostream>
#include <unordered_map>
#include <queue>
#include <sstream>
struct Stringmes {
std::string s;
int count;
int priority;
Stringmes():s(""), count(0), priority(0) {}
};
struct Compare
{
/* data */
bool operator() (const Stringmes& a, const Stringmes& b) const {
if (a.count != b.count) {
return a.count < b.count;
}
else {
return a.priority > b.priority;
}
}
};
int main() {
//处理输入的字符串,存入字符数组
std::string s;
std::getline(std::cin, s);
std::stringstream ss(s);
std::string token;
std::vector<std::string> svec;
while (std::getline(ss, token, ',')) {
svec.push_back(token);
}
//用map来去重,定义一个struct来存储每个ket的count和priority
std::unordered_map<std::string, Stringmes> stringmap;
for (int i = 0; i < svec.size(); i++) {
if (stringmap.find(svec[i]) == stringmap.end()) {
stringmap[svec[i]] = Stringmes();
stringmap[svec[i]].priority = i;
stringmap[svec[i]].s = svec[i];
}
stringmap[svec[i]].count ++;
}
//将map存入优先队列,根据需要设置优先级
std::priority_queue<Stringmes, std::vector<Stringmes>, Compare> pq;
for (const auto & pair: stringmap) {
pq.push(pair.second);
}
//打印优先队列
while(!pq.empty()) {
std::cout << pq.top().s << " ";
pq.pop();
}
}
Hjk螺旋数字矩阵
疫情期间,小明隔离在家,百无聊赖,在纸上写数字玩。他发明了一种写法:
给出数字个数 n (0 < n ≤ 999)和行数 m(0 < m ≤ 999),从左上角的 1 开始,按照顺时针螺旋向内写方式,依次写出2,3,…,n,最终形成一个 m 行矩阵。
小明对这个矩阵有些要求:
每行数字的个数一样多
列的数量尽可能少
填充数字时优先填充外部
数字不够时,使用单个 * 号占位
输入描述
两个整数,空格隔开,依次表示 n、m
输出描述
符合要求的唯一矩阵
用例
输入 9 4
输出
1 2 3
* * 4
9 * 5
8 7 6
说明:9个数字写出4行,最少需要3列
输入 3 5
输出
1
2
3
*
*
说明:3个数字写5行,只有一列,数字不够用*号填充
输入 120 7
输出
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 19
45 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 63 20
44 83 114 115 116 117 118 119 120 * * * * * * 99 64 21
43 82 113 112 111 110 109 108 107 106 105 104 103 102 101 100 65 22
42 81 80 79 78 77 76 75 74 73 72 71 70 69 68 67 66 23
41 40 39 38 37 36 35 34 33 32 31 30 29 28 27 26 25 24
说明 无
解题思路
首先确定螺旋矩阵的大小。由于要填入的数字范围是1到n,且要求行数m,列数k尽可能小,因此可以计算出矩阵的行数为m,列数为k,其中k等于n除以m的向上取整(即Math.ceil(n * 1.0 / m))。
创建一个大小为m行k列的二维数组作为螺旋矩阵。
初始化填值过程的变量,包括当前要填入的值step,以及当前填入值的位置x和y。
使用循环进行填值过程,直到填入的值step大于n为止:
首先在当前行中从左向右填值,直到到达矩阵的右边界或者遇到已经填入值的位置。
然后在当前列中从上到下填值,直到到达矩阵的下边界或者遇到已经填入值的位置。
接着在当前行中从右向左填值,直到到达矩阵的左边界或者遇到已经填入值的位置。
最后在当前列中从下到上填值,直到到达矩阵的上边界或者遇到已经填入值的位置。
每次填入一个值后,更新step的值,并移动填值位置的坐标。
如果填值过程中遇到已经填入值的位置或者越界,则停止填值,整个填值过程结束。
#include <iostream>
#include <vector>
#include <cmath>
//求得螺旋矩阵
std::vector<std::vector<int>> getMatrix(const int& n, const int &m, const int &k) {
std::vector<std::vector<int>> matrix(m, std::vector<int>(k, 0));
int x = 0, y = 0;
int count = 1;
while (count <= n) {
//上面左到右(到矩阵边界或遇到已经填入的)
while ( y < k && matrix[x][y] == 0 && count <= n) { //条件判断的顺序要先判断y < k
matrix[x][y++] = count ++;
}
y -= 1;
x += 1; //移动到下一次的起始位置
//右边上到下
while (x < m && matrix[x][y] == 0 && count <= n) {
matrix[x++][y] = count ++;
}
x -= 1;
y -= 1;
//下边右到左
while (y >= 0 &&matrix[x][y] == 0 && count <= n) {
matrix[x][y--] = count ++;
}
x -= 1;
y += 1;
//左边下到上
while (x >= 0 && matrix[x][y] == 0 && count <= n) {
matrix[x--][y] = count ++;
}
x += 1;
y += 1;
}
return matrix;
}
int main() {
//处理输入,求列数
int n, m;
std::cin >> n >> m;
int k = std::ceil(static_cast<double>(n) / m);
// std::cout << n << " " << m << " " << k <<std::endl;
std::vector<std::vector<int>> matrix = getMatrix(n, m, k);
for (auto i : matrix) {
for (int j : i) {
if (j != 0) {
std::cout << j << " ";
}
else {
std::cout << "*" << " ";
}
}
std::cout << std::endl;
}
return 0;
}
Hjk数组连续和
【数组连续和】
给定一个含有N个正整数的数组, 求出有多少个连续区间(包括单个正整数), 它们的和大于等于x。
输入描述
第一行两个整数N x(0 < N <= 100000, 0 <= x <= 10000000)
第二行有N个正整数(每个正整数小于等于100)。
输出描述
输出一个整数,表示所求的个数。
注意:*此题对效率有要求,暴力解法通过率不高,请考虑高效的实现方式。*
示例1 输入输出示例仅供调试,后台判题数据一般不包含示例
输入
3 7
3 4 7
输出
4
样例解释
第一行的3表示第二行数组输入3个数,第一行的7是比较数,用于判断连续数组是否大于该数;
组合为 3 + 4; 3 + 4 + 7; 4 + 7; 7; 都大于等于指定的7;所以共四组。
示例2 输入输出示例仅供调试,后台判题数据一般不包含示例
10 10000
1 2 3 4 5 6 7 8 9 10
样例解释
所有元素的和小于10000,所以返回0。
解题思路
当我们遍历数组时,我们需要记录当前连续区间的和,以及当前连续区间的左边界和右边界。我们使用双指针来实现这一点,即 left 和 right。初始时,左指针和右指针都指向数组的第一个元素。
然后我们开始向右移动右指针 right,并在移动过程中不断累加当前连续区间的和 sum。当 sum 大于等于目标值 x 时,说明当前连续区间的和满足条件,这时我们就可以计算出以当前右指针结尾的连续区间的个数。具体而言,以当前右指针结尾的连续区间个数为 N - right,因为对于当前右指针 right,以它为结尾的连续区间分别是从 left 到 right、从 left + 1 到 right、…、从 right 到 right。累加这些连续区间的个数即可得到以当前右指针结尾的连续区间的个数。
接着,我们将左指针向右移动一位,同时减去移动前左指针所指向的元素的值,更新 sum。我们重复这一过程,直到右指针移动到数组的最后一个元素为止。
最后,我们累加所有以不同右指针结尾的连续区间的个数,就得到了满足条件的连续区间的总个数。
#include <iostream>
#include <vector>
int main()
{
int N, x;
std::cin >> N >> x;
std::vector<int> nums(N, 0);
for (int i = 0; i < N; i++)
{
std::cin >> nums[i];
}
int count = 0, sum = 0;
int left = 0;
for (int right = 0; right < N; right++)
{
sum += nums[right];
while (sum >= x)
{
count += (N - right);
sum -= nums[left];
left++;
}
}
std::cout << count << std::endl;
return 0;
}
Hjk分割数组的最大差值
给定一个由若干整数组成的数组nums ,可以在数组内的任意位置进行分割,将该数组分割成两个非空子数组(即左数组和右数组),分别对子数组求和得到两个值,计算这两个值的差值,请输出所有分割方案中,差值最大的值。
输入描述
第一行输入数组中元素个数n,1 < n <= 100000
第二行输入数字序列,以空格进行分隔,数字取值为4字节整数
输出描述
输出差值的最大取值
示例1:
输入:
6
1 -2 3 4 -9 7
输出:
10
说明:
将数组 nums 划分为两个非空数组的可行方案有:左数组 = [1] 且 右数组 = [-2,3,4,-9,7],和的差值 = |1 - 3|=2
左数组 = [1,-2] 且 右数组 = [3,4,-9,7],和的差值 =| -1-5 |=6
左数组 =[1,-2,3,1] 且 右数组 =[4,-9,7],和的差值 =|2 - 2|=0
左数组 =[1,-2,3,4] 且右数组=[-9,7],和的差值 =|6 -(-2)| = 8,
左数组 =[1,-2,3,4,-9] 且 右数组 = [7],和的差值 =|-3-7| = 10最大的差值为10
#include <iostream>
#include <vector>
int main() {
int n;
std::cin >> n;
std::vector<int> nums(n, 0);
for (int i = 0; i < nums.size(); i ++) {
std::cin >> nums[i];
}
int rightsum = 0, leftsum = 0;
for (int i : nums) {
rightsum += i;
}
int subdifference = 0;
for (int i = 0; i < nums.size(); i ++) {
leftsum += nums[i];
rightsum -= nums[i];
subdifference = std::max(std::abs(leftsum - rightsum), subdifference);
}
std::cout << subdifference << std::endl;
return 0;
}
Hjk数字整除
小明正在玩一种特别的牌游戏。这个游戏的玩法如下:
小明先拿到一张牌,上面有一个数字m。
然后,他会依次拿到n张牌,这些牌连成一排。
小明的挑战是:从这n张牌中,找到连在一起的一串牌,使它们的数字和能被m整除。
你的任务: 对于每一轮游戏,判断小明是否能完成挑战。
输入:
第一行包含两个整数:n和m。
第二行包含n个整数,代表n张牌上的数字。
输出:
如果小明可以找到符合条件的一串牌,输出1。
如果找不到,输出0。
输入
6 7
2 12 6 3 5 5
10 11
1 1 1 1 1 1 1 1 1 1
输出
1
0
数学原理
根据模运算的性质,如果两个数除以同一个数m
的余数相等,那么这两个数相减的结果一定是m
的倍数。用公式表示就是:
如果s_i % m = s_j % m
,则有(s_j - s_i) % m = 0
。
这里,s_j - s_i
实际上就是a_(i+1) + a_(i+2) + ... + a_j
的和,即从第i+1
个元素到第j
个元素的子数组的和。
结论
所以,当我们在计算前缀和的过程中发现某个余数已经出现过,这意味着我们可以从之前余数出现的位置到当前位置的子数组,找到一个和能被m
整除的子数组。这个子数组的和就是两个具有相同余数的前缀和之差,它是m
的倍数。
题目分析
- 初始化一个哈希表,用来记录前缀和对
m
的余数出现的次数。 - 遍历数组,计算每一步的前缀和,并计算这个前缀和除以
m
的余数。 - 如果这个余数在之前已经出现过(即哈希表中已存在该余数),则说明我们找到了一组子数组,其和能被
m
整除。这是因为从上一个具有相同余数的前缀和到当前的前缀和,这一段子数组的和必定是m
的倍数。 - 特别地,如果在任何时刻前缀和本身就直接被
m
整除(即余数为0),或者余数为0的情况在哈希表中已经记录过,则也说明找到了这样的一组子数组。
#include <iostream>
#include <vector>
#include <unordered_map>
int main() {
int n, m;
std::cin >> n >> m;
std::vector<int> cards(n, 0);
for (int i = 0; i < n; i++) {
std::cin >> cards[i];
}
//存前缀和与m的余数
std::unordered_map<int, int> remaindermap;
remaindermap[0] = 1; //初始化,为了处理整个子数组和就是m的倍数情况
int subsum = 0;
bool find = false;
for (int i = 0; i < n; i++) {
subsum += cards[i];
int remainder = subsum % m;
if(remaindermap.find(remainder) != remaindermap.end()) {
find = true;
break;
}
remaindermap[remainder] ++;
}
std::cout << (find? 1 : 0) << std::endl;
return 0;
}