1. 顺序表基础回顾与笔试题特点
顺序表作为线性表最基础的存储结构,是数据结构入门必须掌握的"必修课"。它的核心在于用一组地址连续的存储单元依次存储数据元素,就像排队买奶茶的顾客,每个人占据固定位置且前后关系明确。
顺序表笔试题目通常考察三个维度:
- 基础操作的时间复杂度分析(比如插入删除为何是O(n))
- 特殊场景下的算法优化(如合并有序顺序表)
- 实际工程问题的解决方案(动态扩容策略)
2. 高频笔试题精讲
2.1 删除重复元素(保留一个)
典型题目:给定非递减顺序表,删除重复元素使每个元素只出现一次。
c复制int removeDuplicates(int* nums, int numsSize){
if(numsSize == 0) return 0;
int slow = 0;
for(int fast = 1; fast < numsSize; fast++){
if(nums[fast] != nums[slow]){
nums[++slow] = nums[fast];
}
}
return slow + 1;
}
关键点:双指针法,slow指向已处理序列末尾,fast探索新元素。时间复杂度O(n),空间复杂度O(1)
2.2 两顺序表合并
合并两个非递减顺序表,要求结果仍有序:
c复制void merge(int* nums1, int m, int* nums2, int n){
int p = m + n - 1;
m--; n--;
while(n >= 0){
nums1[p--] = (m >=0 && nums1[m] > nums2[n]) ? nums1[m--] : nums2[n--];
}
}
注意:从后向前填充可避免数据覆盖,这是工程实践中常用的技巧
3. 进阶难题解析
3.1 环形缓冲区实现
环形队列是顺序表的典型应用,关键在取模运算:
c复制#define MAX_SIZE 100
typedef struct {
int data[MAX_SIZE];
int front, rear;
} CircularQueue;
bool enQueue(CircularQueue* obj, int value) {
if((obj->rear + 1) % MAX_SIZE == obj->front)
return false;
obj->data[obj->rear] = value;
obj->rear = (obj->rear + 1) % MAX_SIZE;
return true;
}
3.2 动态扩容策略
当顺序表空间不足时,常见的扩容方案:
- 固定步长扩容(每次增加固定容量)
- 倍数扩容(Java ArrayList采用1.5倍)
- 自适应策略(根据历史使用情况预测)
c复制void expandList(List* list) {
int newCapacity = list->capacity * 2; // 倍数扩容
ElemType* newData = (ElemType*)malloc(sizeof(ElemType)*newCapacity);
for(int i=0; i<list->length; i++){
newData[i] = list->data[i];
}
free(list->data);
list->data = newData;
list->capacity = newCapacity;
}
4. 实战技巧与避坑指南
4.1 边界检查黄金法则
- 插入/删除前检查索引有效性
- 操作空表前判断length是否为0
- 满表操作前检查capacity
- 指针操作前验证非NULL
c复制// 错误示范
void insert(List* list, int index, ElemType e){
// 缺少边界检查直接操作
for(int i=list->length; i>index; i--){
list->data[i] = list->data[i-1];
}
list->data[index] = e;
}
// 正确做法
Status insert(List* list, int index, ElemType e){
if(index<0 || index>list->length) return ERROR;
if(list->length == list->capacity) expandList(list);
/* 后续操作 */
}
4.2 内存管理要点
- malloc/calloc后必须检查返回值
- free后立即置NULL防止野指针
- 避免"内存泄漏三明治"(连续申请不释放)
- 使用valgrind等工具定期检测
5. 经典题型解题模板
5.1 双指针法
适用场景:原地修改、删除元素、滑动窗口
c复制// 移除指定值元素
int removeElement(int* nums, int numsSize, int val){
int slow = 0;
for(int fast=0; fast<numsSize; fast++){
if(nums[fast] != val){
nums[slow++] = nums[fast];
}
}
return slow;
}
5.2 逆序处理法
适用场景:大数加法、多项式运算
c复制// 大数加1(数字用数组存储)
int* plusOne(int* digits, int digitsSize, int* returnSize){
for(int i=digitsSize-1; i>=0; i--){
digits[i]++;
digits[i] %= 10;
if(digits[i] != 0) break;
}
/* 处理全9进位情况 */
}
6. 性能优化实战
6.1 缓存友好设计
- 顺序访问优于随机访问
- 结构体大小对齐到缓存行(通常64字节)
- 热点数据集中存储
c复制// 不好的设计
typedef struct {
int id; // 4字节
char name[8]; // 8字节
double score; // 8字节(可能跨缓存行)
} Student;
// 优化后
typedef struct {
double score; // 8
int id; // 4
char name[4]; // 4(补齐到16字节整数倍)
} Student_Opt;
6.2 批量操作优化
单次扩容10倍比10次扩容1倍更高效:
c复制// 低效做法
for(int i=0; i<1000; i++){
if(list->length == list->capacity){
list->capacity += 10; // 频繁扩容
list->data = realloc(list->data, list->capacity*sizeof(ElemType));
}
list->data[list->length++] = i;
}
// 优化方案
int targetSize = list->length + 1000;
if(targetSize > list->capacity){
list->capacity = targetSize * 1.5; // 预扩容
list->data = realloc(list->data, list->capacity*sizeof(ElemType));
}
/* 批量插入 */
7. 企业级应用案例
7.1 Redis的SDS实现
Redis字符串(Simple Dynamic String)采用顺序表优化:
- 预分配空间减少内存分配次数
- 惰性空间释放提升性能
- 二进制安全(可存储任意数据)
c复制struct sdshdr {
int len; // 已用长度
int free; // 剩余空间
char buf[]; // 柔性数组
};
// 创建字符串
sds sdsnewlen(const void *init, size_t initlen) {
struct sdshdr *sh;
sh = malloc(sizeof(struct sdshdr)+initlen+1);
sh->len = initlen;
sh->free = 0;
/* 数据初始化 */
return (char*)sh->buf;
}
7.2 Nginx数组实现
Nginx动态数组的精妙设计:
- 每个元素大小固定
- 支持快速随机访问
- 内存池配合使用
c复制typedef struct {
void *elts; // 数组首地址
ngx_uint_t nelts; // 当前元素数
size_t size; // 单个元素大小
ngx_uint_t nalloc; // 总容量
ngx_pool_t *pool; // 内存池
} ngx_array_t;
8. 调试与测试技巧
8.1 单元测试要点
- 边界测试:空表、满表、单元素表
- 异常测试:非法索引、空指针
- 性能测试:大规模数据操作
c复制void test_SeqList() {
// 初始化测试
List L;
initList(&L);
assert(L.length == 0);
// 边界测试
insert(&L, 0, 1); // 空表插入
assert(L.data[0] == 1);
// 异常测试
Status ret = insert(NULL, 0, 1);
assert(ret == ERROR);
}
8.2 GDB调试命令
bash复制# 常用命令
break list.c:35 # 在35行设断点
watch L->length # 监视length变化
backtrace # 查看调用栈
x/10xw L->data # 查看数组内存
9. 不同语言实现对比
9.1 C++ vector实现要点
- 使用模板支持泛型
- 迭代器失效问题
- 异常安全保证
cpp复制template<typename T>
class Vector {
private:
T* _data;
size_t _size;
size_t _capacity;
public:
void push_back(const T& val) {
if(_size == _capacity) {
reserve(_capacity * 2);
}
_data[_size++] = val;
}
};
9.2 Python list的优化
- 存储的是对象指针而非对象本身
- 过度分配策略:0,4,8,16,25,35,46,58,72,88,...
- 垃圾回收机制处理循环引用
python复制# 查看列表内存分配
import sys
lst = []
for i in range(100):
print(f"len={len(lst)}, alloc={sys.getsizeof(lst)}")
lst.append(i)
10. 扩展思考题
- 如何实现O(1)时间复杂度的随机删除操作?
- 设计支持快速查找最大/最小值的特殊顺序表
- 多线程环境下的安全顺序表实现
- 持久化顺序表(支持版本回溯)
c复制// 思考题1的解决方案:交换删除法
Status deleteRandom(List* list, int index){
if(index<0 || index>=list->length) return ERROR;
list->data[index] = list->data[--list->length];
return OK;
}
在实际面试中,面试官往往更关注你对时间/空间复杂度的分析能力,以及面对特殊约束时的应变思路。建议在理解基础实现后,多思考各种变形问题的解决方案。
