1. 一维数组基础与经典算法题解析
在编程学习的初级阶段,一维数组是最基础也是最重要的数据结构之一。它不仅是理解更复杂数据结构的基础,也是解决实际问题的利器。今天我将通过三个经典题目,带大家深入掌握一维数组的应用技巧。
1.1 黑色星期五问题(基础题37th)
黑色星期五问题要求我们计算某个月的第13天是星期五的次数。这个题目看似简单,但涉及多个编程基础知识点:
c复制#include <stdio.h>
int main() {
int days_in_month[] = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
int year, month, day;
int total_days = 0;
int friday13_count = 0;
// 假设我们计算1900-2000年间的黑色星期五
for(year = 1900; year <= 2000; year++) {
// 判断闰年
if((year % 4 == 0 && year % 100 != 0) || (year % 400 == 0)) {
days_in_month[1] = 29;
} else {
days_in_month[1] = 28;
}
for(month = 0; month < 12; month++) {
// 检查当月13号是否是周五
if((total_days + 13) % 7 == 5) {
friday13_count++;
printf("%d年%d月13日是黑色星期五\n", year, month+1);
}
total_days += days_in_month[month];
}
}
printf("1900-2000年间共有%d个黑色星期五\n", friday13_count);
return 0;
}
这个实现有几个关键点需要注意:
- 使用一维数组存储每月天数,简洁高效
- 闰年判断逻辑要准确,特别是世纪年的特殊情况
- 总天数累加时要注意数组索引从0开始
提示:在实际应用中,可以使用Zeller公式等更高效的算法来计算星期几,但数组解法更直观易懂,适合初学者理解。
1.2 树结构的一维数组表示(基础题38th)
树结构通常用指针或对象表示,但在某些场景下,用一维数组表示树结构可以带来性能优势。以下是二叉树的一维数组表示方法:
c复制#define MAX_SIZE 100
int tree[MAX_SIZE];
// 初始化树
void init_tree() {
for(int i = 0; i < MAX_SIZE; i++) {
tree[i] = -1; // -1表示空节点
}
}
// 插入节点
void insert(int value, int index) {
if(index >= MAX_SIZE) {
printf("树已满\n");
return;
}
tree[index] = value;
}
// 获取左子节点索引
int left_child(int index) {
return 2 * index + 1;
}
// 获取右子节点索引
int right_child(int index) {
return 2 * index + 2;
}
// 前序遍历
void preorder(int index) {
if(index >= MAX_SIZE || tree[index] == -1) return;
printf("%d ", tree[index]);
preorder(left_child(index));
preorder(right_child(index));
}
这种表示方法的优缺点:
- 优点:
- 访问速度快,不需要指针跳转
- 内存连续,缓存友好
- 适合完全二叉树或接近完全的二叉树
- 缺点:
- 稀疏树会浪费大量空间
- 插入删除操作可能导致大量数据移动
注意:在实际工程中,B树、B+树等更复杂的树结构也可以用类似方式实现,但需要考虑节点分裂合并等操作。
1.3 约瑟夫环问题(基础题39th)
约瑟夫环是一个经典的数学问题,描述的是n个人围成一圈,从某个指定的人开始报数,数到k的那个人就被淘汰,然后从下一个人重新开始报数,直到所有人都被淘汰。用一维数组可以高效模拟这个过程:
c复制#include <stdio.h>
#include <stdlib.h>
void josephus(int n, int k) {
int *circle = (int *)malloc(n * sizeof(int));
for(int i = 0; i < n; i++) {
circle[i] = i + 1; // 编号1-n
}
int remaining = n;
int current = 0;
int count = 0;
while(remaining > 0) {
if(circle[current] != 0) {
count++;
if(count == k) {
printf("%d ", circle[current]);
circle[current] = 0;
remaining--;
count = 0;
}
}
current = (current + 1) % n;
}
free(circle);
}
int main() {
josephus(7, 3); // 经典约瑟夫环问题:7个人,数到3淘汰
return 0;
}
这个实现有几个优化点:
- 使用取模运算实现环形遍历
- 标记淘汰的人为0,避免实际删除元素
- 时间复杂度为O(n*k),对于小规模数据足够高效
对于大规模数据,可以使用数学公式递归求解,时间复杂度可以降到O(n):
c复制int josephus_math(int n, int k) {
if(n == 1) return 0;
return (josephus_math(n - 1, k) + k) % n;
}
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 一维数组的高级应用技巧
掌握了基础题目后,我们来看一些一维数组的高级应用技巧,这些技巧在实际编程面试和工程实践中都非常有用。
2.1 原地算法与空间优化
很多数组问题可以通过原地操作来优化空间复杂度。例如,删除排序数组中的重复项:
c复制int removeDuplicates(int* nums, int numsSize) {
if(numsSize == 0) return 0;
int i = 0;
for(int j = 1; j < numsSize; j++) {
if(nums[j] != nums[i]) {
i++;
nums[i] = nums[j];
}
}
return i + 1;
}
这个算法的核心思想是使用双指针:
- 慢指针i指向当前唯一元素的最后一个位置
- 快指针j遍历数组
- 当发现新元素时,将其复制到i+1位置
时间复杂度O(n),空间复杂度O(1),比创建新数组的方法高效得多。
2.2 滑动窗口技巧
滑动窗口是处理子数组/子串问题的利器。例如,求大小为k的子数组的最大和:
c复制int maxSubarraySum(int arr[], int n, int k) {
if(n < k) return -1;
int max_sum = 0;
for(int i = 0; i < k; i++) {
max_sum += arr[i];
}
int window_sum = max_sum;
for(int i = k; i < n; i++) {
window_sum += arr[i] - arr[i - k];
if(window_sum > max_sum) {
max_sum = window_sum;
}
}
return max_sum;
}
滑动窗口的关键点:
- 先计算初始窗口的和
- 然后滑动窗口,每次加上新元素,减去离开窗口的元素
- 比较并更新最大值
这种方法将O(n*k)的暴力解法优化为O(n),效率提升显著。
2.3 前缀和与差分数组
前缀和数组可以快速计算任意区间的和,在频繁查询的场景下非常有用:
c复制void prefixSum(int arr[], int n) {
int prefix[n];
prefix[0] = arr[0];
for(int i = 1; i < n; i++) {
prefix[i] = prefix[i-1] + arr[i];
}
// 查询区间[i,j]的和:prefix[j] - prefix[i-1]
}
差分数组则适用于频繁区间更新的场景:
c复制void differenceArray(int arr[], int n) {
int diff[n];
diff[0] = arr[0];
for(int i = 1; i < n; i++) {
diff[i] = arr[i] - arr[i-1];
}
// 区间[i,j]增加val:diff[i] += val; diff[j+1] -= val;
// 恢复原数组:arr[0] = diff[0]; arr[i] = arr[i-1] + diff[i];
}
提示:前缀和与差分数组经常用于解决复杂的区间查询和更新问题,是算法竞赛中的常用技巧。
3. 树结构的一维数组表示进阶
3.1 堆的实现与应用
堆是一种特殊的完全二叉树,非常适合用一维数组表示。以下是最大堆的实现:
c复制#define MAX_HEAP_SIZE 100
int heap[MAX_HEAP_SIZE];
int heap_size = 0;
void swap(int *a, int *b) {
int temp = *a;
*a = *b;
*b = temp;
}
void heapify_up(int index) {
while(index > 0 && heap[(index-1)/2] < heap[index]) {
swap(&heap[(index-1)/2], &heap[index]);
index = (index-1)/2;
}
}
void heapify_down(int index) {
int largest = index;
int left = 2*index + 1;
int right = 2*index + 2;
if(left < heap_size && heap[left] > heap[largest])
largest = left;
if(right < heap_size && heap[right] > heap[largest])
largest = right;
if(largest != index) {
swap(&heap[index], &heap[largest]);
heapify_down(largest);
}
}
void insert(int value) {
if(heap_size >= MAX_HEAP_SIZE) return;
heap[heap_size] = value;
heapify_up(heap_size);
heap_size++;
}
int extract_max() {
if(heap_size <= 0) return -1;
int max = heap[0];
heap[0] = heap[heap_size-1];
heap_size--;
heapify_down(0);
return max;
}
堆的应用场景非常广泛:
- 优先级队列
- 堆排序
- Top K问题
- Dijkstra算法等图算法
3.2 线段树实现区间查询
线段树是另一种常用的一维数组表示方法,适合处理区间查询和更新:
c复制#define MAX_N 1000
#define LEFT(i) (2*i + 1)
#define RIGHT(i) (2*i + 2)
int arr[MAX_N];
int tree[4*MAX_N];
void build(int node, int start, int end) {
if(start == end) {
tree[node] = arr[start];
return;
}
int mid = (start + end) / 2;
build(LEFT(node), start, mid);
build(RIGHT(node), mid+1, end);
tree[node] = tree[LEFT(node)] + tree[RIGHT(node)];
}
void update(int node, int start, int end, int idx, int val) {
if(start == end) {
arr[idx] = val;
tree[node] = val;
return;
}
int mid = (start + end) / 2;
if(idx <= mid)
update(LEFT(node), start, mid, idx, val);
else
update(RIGHT(node), mid+1, end, idx, val);
tree[node] = tree[LEFT(node)] + tree[RIGHT(node)];
}
int query(int node, int start, int end, int l, int r) {
if(r < start || l > end) return 0;
if(l <= start && end <= r) return tree[node];
int mid = (start + end) / 2;
return query(LEFT(node), start, mid, l, r) +
query(RIGHT(node), mid+1, end, l, r);
}
线段树的构建和使用要点:
- 线段树需要4倍于原数组的空间
- 构建时间复杂度O(n)
- 查询和更新时间复杂度O(logn)
- 可以支持多种区间操作(求和、最大值、最小值等)
4. 约瑟夫环问题的变种与优化
4.1 不同数据规模的解法选择
约瑟夫环问题的解法需要根据数据规模来选择:
-
小规模数据(n < 1000):
- 直接模拟法足够高效
- 代码简单,易于理解和调试
-
中等规模数据(1000 < n < 10^6):
- 数学递归法
- 时间复杂度O(n),空间复杂度O(1)
-
超大规模数据(n > 10^6):
- 需要进一步优化递归法
- 当k较小时,可以批量处理多个步骤
c复制int josephus_large(int n, int k) {
int res = 0;
for(int i = 2; i <= n; ) {
if(res + k >= i) {
res = (res + k) % i;
i++;
continue;
}
int steps = (i - 1 - res - 1) / k + 1;
if(i + steps > n + 1) {
steps = n + 1 - i;
}
res += k * steps;
i += steps;
}
return res;
}
4.2 约瑟夫环的变种问题
-
找出第m个被淘汰的人:
- 修改模拟法,记录淘汰顺序
- 或者推导数学公式直接计算
-
不同的淘汰规则:
- 每次变化的k值
- 双向淘汰等
-
幸存者位置预测:
- 不实际模拟,直接计算最后幸存者的位置
c复制// 找出第m个被淘汰的人(1-based)
int josephus_mth(int n, int k, int m) {
if(m == 1) return (k-1) % n;
return (josephus_mth(n-1, k, m-1) + k) % n;
}
4.3 实际应用场景
约瑟夫环问题不仅仅是理论题目,在实际中也有应用:
- 资源分配问题
- 进程调度算法
- 密码学中的某些算法
- 游戏设计中的角色淘汰机制
理解约瑟夫环问题的各种解法,可以帮助我们在面对类似循环淘汰的场景时,选择最合适的解决方案。
