一、数据结构入门:从数组说起
数据结构就像我们生活中的储物柜,数组是最简单的"格子间"。它最大的特点就是所有元素在内存中连续排列,就像超市货架上挨着的饮料瓶。
在C#中,数组是这样工作的:
// 技术栈:C#/.NET 6
// 创建包含5个整数的数组
int[] scores = new int[5] { 90, 85, 78, 92, 88 };
// 访问第三个元素(注意索引从0开始)
Console.WriteLine($"第三位成绩:{scores[2]}"); // 输出:78
// 修改最后一个元素
scores[4] = 95;
// 遍历数组的现代写法
foreach (var score in scores)
{
Console.Write($"{score} "); // 输出:90 85 78 92 95
}
应用场景:
- 固定长度的数据集合(如学生成绩表)
- 需要快速随机访问的场景(索引访问时间复杂度O(1))
注意事项:
- 数组长度不可变,需要提前确定大小
- 插入/删除元素时需要移动后续所有元素
二、灵活变通的链表
如果说数组是固定座位的教室,链表就像可以随时加座的咖啡厅。每个元素(节点)都记住下一个伙伴的位置。
C#实现单向链表:
// 技术栈:C#/.NET 6
public class ListNode
{
public int Value { get; set; }
public ListNode Next { get; set; }
public ListNode(int value) => Value = value;
}
// 创建链表:1 → 3 → 5
var head = new ListNode(1);
head.Next = new ListNode(3);
head.Next.Next = new ListNode(5);
// 遍历链表
var current = head;
while (current != null)
{
Console.Write($"{current.Value}→");
current = current.Next; // 输出:1→3→5→
}
技术对比:
- 插入效率:链表O(1) vs 数组O(n)
- 访问效率:链表O(n) vs 数组O(1)
实际应用:
- 浏览器历史记录(频繁增删)
- 内存池管理系统
三、后进先出的栈
栈就像叠盘子,最后放上去的总是最先被拿走。C#中可以用Stack<T>类:
// 技术栈:C#/.NET 6
var browserHistory = new Stack<string>();
// 压栈操作
browserHistory.Push("首页");
browserHistory.Push("产品页");
browserHistory.Push("购物车");
// 查看栈顶
Console.WriteLine($"当前页面:{browserHistory.Peek()}"); // 输出:购物车
// 退栈操作
while (browserHistory.Count > 0)
{
Console.WriteLine($"返回:{browserHistory.Pop()}");
// 输出顺序:购物车 → 产品页 → 首页
}
典型场景:
- 函数调用栈
- 撤销操作(Undo)实现
- 括号匹配检查
四、先进先出的队列
队列就像食堂排队,先来的人先打饭。.NET提供了Queue<T>类:
// 技术栈:C#/.NET 6
var printerQueue = new Queue<string>();
// 入队
printerQueue.Enqueue("财务报告.docx");
printerQueue.Enqueue("季度报表.xlsx");
printerQueue.Enqueue("会议纪要.pdf");
// 处理队列
while (printerQueue.Count > 0)
{
var file = printerQueue.Dequeue();
Console.WriteLine($"正在打印:{file}");
// 输出顺序与入队顺序一致
}
扩展应用:
- 消息队列系统(如RabbitMQ的基础)
- 多线程任务调度
- 广度优先搜索算法
五、技术选型指南
当我们需要选择数据结构时,可以这样考虑:
访问模式:
- 频繁随机访问 → 数组
- 只从两端操作 → 栈/队列
内存考虑:
- 内存紧张 → 数组(无额外指针开销)
- 需要动态扩展 → 链表
线程安全:
.NET提供了ConcurrentStack和ConcurrentQueue等线程安全版本
六、实战:用数组实现栈
为了加深理解,我们手动实现栈:
// 技术栈:C#/.NET 6
public class ArrayStack<T>
{
private T[] _items;
private int _top = -1;
public ArrayStack(int capacity) => _items = new T[capacity];
public void Push(T item)
{
if (_top == _items.Length - 1)
throw new StackOverflowException();
_items[++_top] = item;
}
public T Pop()
{
if (_top == -1)
throw new InvalidOperationException("栈为空");
return _items[_top--];
}
}
// 使用示例
var stack = new ArrayStack<int>(3);
stack.Push(10);
stack.Push(20);
Console.WriteLine(stack.Pop()); // 输出20
七、性能陷阱与优化
数组扩容:
List在.NET中的扩容策略是每次翻倍,频繁扩容会影响性能 缓存友好性:
数组的连续内存特性对CPU缓存更友好GC压力:
链表会产生更多内存碎片,增加GC负担
八、现代.NET中的增强功能
.NET Core以后新增的特性:
// 技术栈:C#/.NET 6
// 范围操作(数组切片)
int[] numbers = { 0, 1, 2, 3, 4 };
var slice = numbers[1..3]; // 包含1, 2
// 只读内存区域
var memory = new ReadOnlyMemory<int>(numbers);
// 高性能Span
Span<int> span = numbers.AsSpan();
span[1] = 99; // 直接修改原数组
九、总结与展望
掌握基础数据结构就像学会了烹饪的基本刀工。虽然现代开发中我们更多使用现成的集合类,但理解它们的原理才能:
- 在性能敏感场景做出正确选择
- 更好地理解高级数据结构(如哈希表、树)
- 应对技术面试中的算法问题
下次当你使用List
评论