C#数据结构
C# 数据结构
1. String(字符串)
1.1 基本概念
- 字符串是C#中最常用的数据类型之一
- 字符串是不可变的(immutable),每次修改都会创建新的字符串对象
- 字符串是引用类型,但具有值类型的特性
1.2 常用操作
1.2.1 字符串创建和连接
// 字符串连接
string str1 = "Hello";
string str2 = "World";
string result = str1 + " " + str2; // 使用 + 运算符
string result2 = string.Concat(str1, " ", str2); // 使用Concat方法
// 字符串格式化
string formatted = string.Format("姓名:{0},年龄:{1}", "张三", 25);
string interpolated = $"姓名:{"张三"},年龄:{25}"; // 字符串插值(C# 6.0+)
1.2.2 字符串查找和替换
string text = "Hello World";
// 查找
bool contains = text.Contains("World"); // 检查是否包含子串
int index = text.IndexOf("o"); // 查找字符或子串的位置
int lastIndex = text.LastIndexOf("o"); // 从后向前查找
// 替换
string replaced = text.Replace("World", "C#"); // 替换子串
1.2.3 字符串分割和合并
// 分割
string[] words = "Hello,World,C#".Split(','); // 按分隔符分割
// 合并
string joined = string.Join("-", words); // 使用分隔符合并数组
1.2.4 字符串修剪和填充
string text = " Hello World ";
// 修剪
string trimmed = text.Trim(); // 去除首尾空格
string trimmedStart = text.TrimStart(); // 去除开头空格
string trimmedEnd = text.TrimEnd(); // 去除结尾空格
// 填充
string padded = text.PadLeft(20, '*'); // 左侧填充
string paddedRight = text.PadRight(20, '*'); // 右侧填充
1.2.5 字符串大小写转换
string text = "Hello World";
string upper = text.ToUpper(); // 转换为大写
string lower = text.ToLower(); // 转换为小写
1.2.6 字符串比较
string str1 = "Hello";
string str2 = "hello";
bool equals = str1.Equals(str2, StringComparison.OrdinalIgnoreCase); // 忽略大小写比较
int compare = string.Compare(str1, str2, StringComparison.OrdinalIgnoreCase); // 比较字符串
// Compare方法返回值说明:
// 返回值 < 0:第一个字符串在字典顺序上小于第二个字符串
// 返回值 = 0:两个字符串相等
// 返回值 > 0:第一个字符串在字典顺序上大于第二个字符串
// 具体示例:
int result1 = string.Compare("apple", "banana"); // 返回 -1,因为'a'的ASCII码(97)小于'b'的ASCII码(98)
int result2 = string.Compare("banana", "apple"); // 返回 1,因为'b'的ASCII码(98)大于'a'的ASCII码(97)
int result3 = string.Compare("apple", "apple"); // 返回 0,因为两个字符串完全相同
1.2.7 字符串截取
string text = "Hello World";
string substring = text.Substring(0, 5); // 从索引0开始截取5个字符
// 更多示例:
string text2 = "ABCDEFG";
string sub1 = text2.Substring(1, 3); // 结果: "BCD" (索引1,2,3)
string sub2 = text2.Substring(2, 2); // 结果: "CD" (索引2,3)
string sub3 = text2.Substring(0, 1); // 结果: "A" (索引0)
1.2.8 字符串格式化
// 数字格式化
string number = string.Format("{0:N2}", 1234.5678); // 输出:1,234.57
string currency = string.Format("{0:C}", 1234.5678); // 输出:¥1,234.57
// 格式说明符说明:
// {0:N2} - N表示数字格式,2表示保留2位小数,自动添加千位分隔符
// {0:C} - C表示货币格式,自动添加货币符号,保留2位小数,添加千位分隔符
// {0:D} - D表示十进制整数格式
// {0:F2} - F表示定点数格式,2表示保留2位小数
// {0:P} - P表示百分比格式,自动乘以100并添加%符号
// {0:X} - X表示十六进制格式
// 更多示例:
string num1 = string.Format("{0:N0}", 1234.5678); // 输出:1,235(无小数)
string num2 = string.Format("{0:F3}", 1234.5678); // 输出:1234.568(3位小数)
string num3 = string.Format("{0:P}", 0.1234); // 输出:12.34%
string num4 = string.Format("{0:X}", 255); // 输出:FF(十六进制)
// 日期格式化
string date = string.Format("{0:yyyy-MM-dd}", DateTime.Now); // 输出:2024-03-21
1.2.9 字符串构建
// 使用StringBuilder(适用于频繁修改字符串的场景)
StringBuilder sb = new StringBuilder();
sb.Append("Hello");
sb.Append(" ");
sb.Append("World");
string result = sb.ToString();
1.2.10 字符串验证
string text = "Hello123";
bool isNullOrEmpty = string.IsNullOrEmpty(text); // 检查是否为空或null
bool isNullOrWhiteSpace = string.IsNullOrWhiteSpace(text); // 检查是否为空、null或只包含空白字符
1.3 性能注意事项
- 字符串是不可变的,频繁修改会导致性能问题
- 对于频繁的字符串操作,应使用StringBuilder
- 字符串比较时要注意使用适当的比较方法
- 在处理大量字符串操作时,要注意性能影响
1.4 最佳实践
- 使用字符串插值($"...")代替string.Format
- 使用StringBuilder进行大量字符串操作
- 使用适当的字符串比较方法
- 注意字符串的不可变性
- 合理使用字符串池(String Pool)
1.5 字符串池(String Pool)详解
字符串池是CLR(Common Language Runtime)中的一个特殊内存区域,用于存储字符串字面量。它的主要目的是优化内存使用。
1.5.1 字符串池的工作原理
// 以下代码中,str1和str2实际上指向同一个字符串对象
string str1 = "Hello";
string str2 = "Hello";
bool areSame = ReferenceEquals(str1, str2); // 返回 true
// 但是通过new创建的字符串不会使用字符串池
string str3 = new string("Hello");
bool areSame2 = ReferenceEquals(str1, str3); // 返回 false
1.5.2 字符串池的优点
- 节省内存:相同的字符串字面量只存储一次
- 提高性能:字符串比较可以直接比较引用
- 减少垃圾回收压力
1.5.3 字符串池的注意事项
- 只有字符串字面量才会被放入字符串池
- 运行时创建的字符串(如通过new或字符串操作)不会被放入字符串池
- 可以使用
string.Intern()方法手动将字符串放入字符串池 - 可以使用
string.IsInterned()方法检查字符串是否在字符串池中
// 手动将字符串放入字符串池
string str4 = "World";
string str5 = string.Intern("World");
bool areSame3 = ReferenceEquals(str4, str5); // 返回 true
// 检查字符串是否在字符串池中
string str6 = "Test";
string interned = string.IsInterned(str6); // 如果在池中,返回池中的引用;否则返回null
1.5.4 使用建议
对于频繁使用的字符串字面量,让它们自然进入字符串池
对于动态生成的字符串,如果确定会重复使用,可以考虑使用 string.Intern()
不要过度使用 string.Intern(),因为字符串池会占用内存直到应用程序结束
在性能关键的场景,可以使用字符串池来优化字符串比较
2. 链表(LinkedList)
2.1 基本概念
- 链表是一种线性数据结构,由一系列节点组成
- 每个节点包含数据和指向下一个节点的引用
- 链表不需要连续的内存空间
- 适合频繁的插入和删除操作
2.2 常见操作
2.2.1 创建链表
// 定义链表节点
public class ListNode
{
public int val;
public ListNode next;
public ListNode(int val) { this.val = val; }
}
// 创建链表
ListNode head = new ListNode(1);
head.next = new ListNode(2);
head.next.next = new ListNode(3);
2.2.2 遍历链表
// 方法1:while循环
ListNode current = head;
while (current != null)
{
Console.WriteLine(current.val);
current = current.next;
}
// 方法2:for循环
for (ListNode node = head; node != null; node = node.next)
{
Console.WriteLine(node.val);
}
2.2.3 插入节点
// 在头部插入
ListNode newHead = new ListNode(0);
newHead.next = head;
head = newHead;
// 在中间插入
ListNode newNode = new ListNode(2);
newNode.next = current.next;
current.next = newNode;
// 在尾部插入
ListNode tail = head;
while (tail.next != null)
{
tail = tail.next;
}
tail.next = new ListNode(4);
2.2.4 删除节点
// 删除头节点
head = head.next;
// 删除中间节点:找到delNode的前驱
current.next = current.next.next;
// 删除尾节点:找到尾节点的前驱
ListNode current = head;
while (current.next.next != null)
{
current = current.next;
}
current.next = null;
2.2.5 查找节点
// 查找特定值
ListNode FindNode(ListNode head, int target)
{
ListNode current = head;
while (current != null)
{
if (current.val == target)
return current;
current = current.next;
}
return null;
}
// 查找中间节点(快慢指针)
ListNode FindMiddle(ListNode head)
{
ListNode slow = head;
ListNode fast = head;
while (fast != null && fast.next != null)
{
slow = slow.next;
fast = fast.next.next;
}
return slow;
}
2.2.6 反转链表
// 迭代方式
ListNode ReverseList(ListNode head)
{
ListNode prev = null;
ListNode current = head;
while (current != null)
{
ListNode next = current.next;
current.next = prev;
prev = current;
current = next;
}
return prev;
}
// 递归方式
ListNode ReverseListRecursive(ListNode head)
{
if (head == null || head.next == null)
return head;
ListNode newHead = ReverseListRecursive(head.next);
head.next.next = head;
head.next = null;
return newHead;
}
2.2.7 检测环
bool HasCycle(ListNode head)
{
if (head == null) return false;
ListNode slow = head;
ListNode fast = head;
while (fast != null && fast.next != null)
{
slow = slow.next;
fast = fast.next.next;
if (slow == fast)
return true;
}
return false;
}
2.3 常见应用场景
- 实现LRU缓存
- 合并有序链表
- 删除链表中的重复元素
- 判断链表是否为回文
- 找到链表的倒数第k个节点
- 链表排序
2.4 性能特点
-
优点:
- 动态大小,不需要预先分配空间
- 插入和删除操作效率高(O(1))
- 不需要连续的内存空间
-
缺点:
- 随机访问效率低(O(n))
- 需要额外的空间存储指针
- 缓存利用率较低
2.5 最佳实践
- 使用虚拟头节点简化操作
- 注意处理边界情况(空链表、单节点等)
- 使用快慢指针解决特定问题
- 注意内存管理,避免内存泄漏
- 合理使用递归和迭代方法
3. 树(Tree)
3.1 基本概念
- 树是一种非线性数据结构,由节点和边组成
- 每个节点可以有多个子节点,但只有一个父节点(根节点除外)
- 常见的树类型:二叉树、二叉搜索树、平衡树、红黑树等
3.2 二叉树的基本操作
3.2.1 定义树节点
public class TreeNode
{
public int val;
public TreeNode left;
public TreeNode right;
public TreeNode(int val) { this.val = val; }
}
3.2.2 遍历方式
// 前序遍历(根-左-右)
void PreorderTraversal(TreeNode root)
{
if (root == null) return;
Console.WriteLine(root.val); // 访问根节点
PreorderTraversal(root.left); // 遍历左子树
PreorderTraversal(root.right);// 遍历右子树
}
// 中序遍历(左-根-右)
void InorderTraversal(TreeNode root)
{
if (root == null) return;
InorderTraversal(root.left); // 遍历左子树
Console.WriteLine(root.val); // 访问根节点
InorderTraversal(root.right); // 遍历右子树
}
// 后序遍历(左-右-根)
void PostorderTraversal(TreeNode root)
{
if (root == null) return;
PostorderTraversal(root.left); // 遍历左子树
PostorderTraversal(root.right); // 遍历右子树
Console.WriteLine(root.val); // 访问根节点
}
// 层序遍历(广度优先)
void LevelOrderTraversal(TreeNode root)
{
if (root == null) return;
Queue<TreeNode> queue = new Queue<TreeNode>();
queue.Enqueue(root);
while (queue.Count > 0)
{
TreeNode node = queue.Dequeue();
Console.WriteLine(node.val);
if (node.left != null)
queue.Enqueue(node.left);
if (node.right != null)
queue.Enqueue(node.right);
}
}
3.2.3 常见操作
// 计算树的高度
int GetHeight(TreeNode root)
{
if (root == null) return 0;
return Math.Max(GetHeight(root.left), GetHeight(root.right)) + 1;
}
// 判断是否为平衡树
bool IsBalanced(TreeNode root)
{
if (root == null) return true;
int leftHeight = GetHeight(root.left);
int rightHeight = GetHeight(root.right);
return Math.Abs(leftHeight - rightHeight) <= 1
&& IsBalanced(root.left)
&& IsBalanced(root.right);
}
// 判断是否为对称树
bool IsSymmetric(TreeNode root)
{
if (root == null) return true;
return IsMirror(root.left, root.right);
}
bool IsMirror(TreeNode left, TreeNode right)
{
if (left == null && right == null) return true;
if (left == null || right == null) return false;
return left.val == right.val
&& IsMirror(left.left, right.right)
&& IsMirror(left.right, right.left);
}
3.3 二叉搜索树(BST)操作
// 查找节点
TreeNode SearchBST(TreeNode root, int val)
{
if (root == null || root.val == val) return root;
if (val < root.val)
return SearchBST(root.left, val);
else
return SearchBST(root.right, val);
}
// 插入节点
TreeNode InsertBST(TreeNode root, int val)
{
if (root == null) return new TreeNode(val);
if (val < root.val)
root.left = InsertBST(root.left, val);
else if (val > root.val)
root.right = InsertBST(root.right, val);
return root;
}
// 删除节点
TreeNode DeleteBST(TreeNode root, int val)
{
if (root == null) return null;
if (val < root.val)
root.left = DeleteBST(root.left, val);
else if (val > root.val)
root.right = DeleteBST(root.right, val);
else
{
// 情况1:叶子节点
if (root.left == null && root.right == null)
return null;
// 情况2:只有一个子节点
else if (root.left == null)
return root.right;
else if (root.right == null)
return root.left;
// 情况3:有两个子节点
else
{
TreeNode minNode = FindMin(root.right);
root.val = minNode.val;
root.right = DeleteBST(root.right, minNode.val);
}
}
return root;
}
TreeNode FindMin(TreeNode root)
{
while (root.left != null)
root = root.left;
return root;
}
3.4 常见应用场景
- 文件系统的目录结构
- 数据库索引(B+树)
- 表达式求值
- 决策树算法
- 哈夫曼编码
- 游戏中的场景树
3.4.1 游戏场景树详解
场景树是游戏引擎中用于组织和管理游戏对象的重要数据结构。它使用树形结构来表示游戏场景中所有对象的层次关系。
// 游戏对象基类
public class GameObject
{
public string name;
public Transform transform;
public List<GameObject> children;
public GameObject parent;
public GameObject(string name)
{
this.name = name;
this.children = new List<GameObject>();
this.transform = new Transform();
}
// 添加子对象
public void AddChild(GameObject child)
{
child.parent = this;
children.Add(child);
}
// 移除子对象
public void RemoveChild(GameObject child)
{
child.parent = null;
children.Remove(child);
}
// 更新对象(递归更新所有子对象)
public virtual void Update()
{
foreach (var child in children)
{
child.Update();
}
}
}
// 变换组件
public class Transform
{
public Vector3 position;
public Vector3 rotation;
public Vector3 scale;
public Transform()
{
position = Vector3.zero;
rotation = Vector3.zero;
scale = Vector3.one;
}
}
// 使用示例
class GameScene
{
public GameObject root;
public GameScene()
{
// 创建场景根节点
root = new GameObject("Scene");
// 创建玩家
GameObject player = new GameObject("Player");
root.AddChild(player);
// 创建玩家武器
GameObject weapon = new GameObject("Weapon");
player.AddChild(weapon);
// 创建敌人
GameObject enemy = new GameObject("Enemy");
root.AddChild(enemy);
}
public void Update()
{
// 更新整个场景
root.Update();
}
}
场景树的主要特点:
-
层次结构:
- 每个游戏对象可以有多个子对象
- 子对象继承父对象的变换(位置、旋转、缩放)
- 形成清晰的场景层次关系
-
常见用途:
- 组织游戏对象
- 管理对象变换
- 实现对象继承
- 优化渲染和物理计算
- 实现场景序列化
-
优势:
- 清晰的层次关系
- 方便的对象管理
- 高效的场景遍历
- 支持对象继承
- 便于实现场景编辑
-
实际应用:
- Unity的GameObject系统
- Unreal Engine的Actor系统
- 2D/3D游戏场景管理
- 游戏对象池
- 场景加载和卸载
3.5 性能特点
-
优点:
- 层次结构清晰
- 查找效率高(平衡树)
- 支持动态操作
-
缺点:
- 可能退化为链表
- 需要额外的空间存储指针
- 实现复杂度较高
3.6 最佳实践
- 根据需求选择合适的树类型
- 注意树的平衡性
- 合理使用递归和迭代
- 注意内存管理
- 考虑使用平衡树(如红黑树、AVL树)提高性能
4. 前缀树(Trie/字典树)
4.1 基本概念
- 前缀树是一种树形数据结构,用于高效地存储和检索字符串
- 每个节点代表一个字符
- 从根节点到某个节点的路径上的字符连接起来,就是该节点对应的字符串
- 常用于实现字典、自动补全、拼写检查等功能
4.2 基本实现
public class TrieNode
{
public Dictionary<char, TrieNode> children;
public bool isEndOfWord; // 标记是否是单词的结尾
public string word; // 存储完整的单词(可选)
public TrieNode()
{
children = new Dictionary<char, TrieNode>();
isEndOfWord = false;
word = null;
}
}
public class Trie
{
private TrieNode root;
public Trie()
{
root = new TrieNode();
}
// 插入单词
public void Insert(string word)
{
TrieNode current = root;
foreach (char c in word)
{
if (!current.children.ContainsKey(c))
{
current.children[c] = new TrieNode();
}
current = current.children[c];
}
current.isEndOfWord = true;
current.word = word;
}
// 查找单词
public bool Search(string word)
{
TrieNode node = SearchNode(word);
return node != null && node.isEndOfWord;
}
// 查找前缀
public bool StartsWith(string prefix)
{
return SearchNode(prefix) != null;
}
// 查找节点
private TrieNode SearchNode(string word)
{
TrieNode current = root;
foreach (char c in word)
{
if (!current.children.ContainsKey(c))
return null;
current = current.children[c];
}
return current;
}
// 获取所有以prefix开头的单词
public List<string> GetWordsWithPrefix(string prefix)
{
List<string> result = new List<string>();
TrieNode node = SearchNode(prefix);
if (node != null)
{
GetAllWords(node, result);
}
return result;
}
// 递归获取所有单词
private void GetAllWords(TrieNode node, List<string> result)
{
if (node.isEndOfWord)
{
result.Add(node.word);
}
foreach (var child in node.children.Values)
{
GetAllWords(child, result);
}
}
}
4.3 使用示例
// 创建字典树
Trie trie = new Trie();
// 插入单词
trie.Insert("apple");
trie.Insert("app");
trie.Insert("application");
trie.Insert("banana");
// 查找单词
bool hasApple = trie.Search("apple"); // 返回 true
bool hasApp = trie.Search("app"); // 返回 true
bool hasApples = trie.Search("apples"); // 返回 false
// 查找前缀
bool hasAppPrefix = trie.StartsWith("app"); // 返回 true
bool hasBanPrefix = trie.StartsWith("ban"); // 返回 true
bool hasCatPrefix = trie.StartsWith("cat"); // 返回 false
// 获取所有以"app"开头的单词
List<string> words = trie.GetWordsWithPrefix("app");
// 返回: ["app", "apple", "application"]
4.4 常见应用场景
-
自动补全
- 搜索引擎的搜索建议
- IDE的代码补全
- 输入法的单词提示
-
拼写检查
- 检查单词是否在字典中
- 提供拼写建议
-
路由表查找
- IP路由表
- 网络包转发
-
字符串匹配
- 多模式字符串匹配
- 敏感词过滤
4.5 性能特点
-
优点:
- 查找效率高(O(m),m为单词长度)
- 空间效率好(共享前缀)
- 支持前缀匹配
- 适合字符串相关操作
-
缺点:
- 空间复杂度较高
- 不适合存储大量短字符串
- 实现相对复杂
4.6 最佳实践
- 根据实际需求选择合适的数据结构
- 考虑使用压缩前缀树(Radix Tree)优化空间
- 注意内存管理
- 合理设计节点结构
- 考虑使用其他数据结构(如哈希表)作为补充
5. 栈(Stack)
5.1 基本概念
- 栈是一种后进先出(LIFO)的线性数据结构
- 只允许在一端进行插入和删除操作
- 插入操作称为入栈(Push)
- 删除操作称为出栈(Pop)
- 查看栈顶元素称为Peek
5.2 基本操作
// 使用C#内置的Stack类
Stack<int> stack = new Stack<int>();
// 入栈
stack.Push(1);
stack.Push(2);
stack.Push(3);
// 出栈
int top = stack.Pop(); // 返回3并移除
// 查看栈顶元素
int peek = stack.Peek(); // 返回2但不移除
// 检查栈是否为空
bool isEmpty = stack.Count == 0;
// 获取栈中元素数量
int count = stack.Count;
5.3 常见应用场景
5.3.1 括号匹配
// 1. 创建栈来存储左括号
Stack<char> stack = new Stack<char>();
// 2. 遍历字符串中的每个字符
foreach (char c in s)
{
// 3. 如果是左括号,入栈
if (c == '(' || c == '[' || c == '{')
{
stack.Push(c);
}
// 4. 如果是右括号
else
{
// 5. 如果栈为空,说明没有匹配的左括号
if (stack.Count == 0) return false;
// 6. 弹出栈顶的左括号
char top = stack.Pop();
// 7. 检查是否匹配
if ((c == ')' && top != '(') ||
(c == ']' && top != '[') ||
(c == '}' && top != '{'))
{
return false;
}
}
}
// 8. 最后检查栈是否为空
return stack.Count == 0;
{
if (stack.Count == 0) return false;
char top = stack.Pop();
if ((c == ')' && top != '(') ||
(c == ']' && top != '[') ||
(c == '}' && top != '{'))
{
return false;
}
}
}
return stack.Count == 0;
}
输入: "{[()]}"
步骤:
1. 遇到 '{',入栈 -> 栈: ['{']
2. 遇到 '[',入栈 -> 栈: ['{', '[']
3. 遇到 '(',入栈 -> 栈: ['{', '[', '(']
4. 遇到 ')',弹出 '(',匹配 -> 栈: ['{', '[']
5. 遇到 ']',弹出 '[',匹配 -> 栈: ['{']
6. 遇到 '}',弹出 '{',匹配 -> 栈: []
7. 栈为空,返回 true
输入: "{[()]"
步骤:
1. 遇到 '{',入栈 -> 栈: ['{']
2. 遇到 '[',入栈 -> 栈: ['{', '[']
3. 遇到 '(',入栈 -> 栈: ['{', '[', '(']
4. 遇到 ')',弹出 '(',匹配 -> 栈: ['{', '[']
5. 遇到 ']',弹出 '[',匹配 -> 栈: ['{']
6. 栈不为空,返回 false
5.3.2 表达式求值
// 1. 创建栈来存储数字
Stack<int> stack = new Stack<int>();
// 2. 遍历每个token
foreach (string token in tokens)
{
// 3. 如果是数字,入栈
if (int.TryParse(token, out int num))
{
stack.Push(num);
}
// 4. 如果是运算符
else
{
// 5. 弹出两个操作数(注意顺序:b是后弹出的,a是先弹出的)
int b = stack.Pop();
int a = stack.Pop();
// 6. 根据运算符进行计算,结果入栈
switch (token)
{
case "+": stack.Push(a + b); break;
case "-": stack.Push(a - b); break;
case "*": stack.Push(a * b); break;
case "/": stack.Push(a / b); break;
}
}
}
// 7. 返回栈顶元素(最终结果)
return stack.Pop();
5.3.3 函数调用栈
// 递归函数的调用栈
void RecursiveFunction(int n)
{
if (n <= 0) return;
Console.WriteLine($"进入函数,n = {n}");
RecursiveFunction(n - 1);
Console.WriteLine($"离开函数,n = {n}");
}
5.3.4 深度优先搜索(DFS)
// 1. 空树检查
if (root == null) return;
// 2. 创建栈并压入根节点
Stack<TreeNode> stack = new Stack<TreeNode>();
stack.Push(root);
// 3. 循环处理栈中的节点
while (stack.Count > 0)
{
// 4. 弹出栈顶节点并访问
TreeNode node = stack.Pop();
Console.WriteLine(node.val);
// 5. 先压入右子节点,再压入左子节点
// 注意:因为栈是后进先出,所以先压右后压左
if (node.right != null)
stack.Push(node.right);
if (node.left != null)
stack.Push(node.left);
}
树的结构:
1
/ \
2 3
/ \
4 5
遍历步骤:
1. 初始状态:栈 = [1]
2. 弹出1,访问1,压入3和2:栈 = [3, 2]
3. 弹出2,访问2,压入5和4:栈 = [3, 5, 4]
4. 弹出4,访问4:栈 = [3, 5]
5. 弹出5,访问5:栈 = [3]
6. 弹出3,访问3:栈 = []
7. 栈空,遍历结束
输出顺序:1 -> 2 -> 4 -> 5 -> 3
5.4 性能特点
-
优点:
- 入栈和出栈操作都是O(1)时间复杂度
- 实现简单
- 内存使用效率高
-
缺点:
- 只能访问栈顶元素
- 不支持随机访问
- 空间大小固定(如果使用数组实现)
5.5 最佳实践
-
使用场景选择:
- 需要后进先出(LIFO)的场景
- 需要回溯操作的场景
- 需要保存历史状态的场景
-
实现建议:
- 使用C#内置的Stack类
- 注意栈的容量限制
- 处理栈空的情况
- 考虑使用泛型栈增加类型安全性
-
常见错误:
- 在空栈上调用Pop或Peek
- 栈溢出(如果使用固定大小数组)
- 忘记处理异常情况
-
优化技巧:
- 使用数组实现固定大小的栈
- 使用链表实现动态大小的栈
- 考虑使用双栈实现队列
6. 队列(Queue)
6.1 基本概念
- 队列是一种先进先出(FIFO)的线性数据结构
- 只允许在一端进行插入操作(入队),在另一端进行删除操作(出队)
- 插入操作称为入队(Enqueue)
- 删除操作称为出队(Dequeue)
- 查看队首元素称为Peek
6.2 基本操作
// 使用C#内置的Queue类
Queue<int> queue = new Queue<int>();
// 入队
queue.Enqueue(1);
queue.Enqueue(2);
queue.Enqueue(3);
// 出队
int front = queue.Dequeue(); // 返回1并移除
// 查看队首元素
int peek = queue.Peek(); // 返回2但不移除
// 检查队列是否为空
bool isEmpty = queue.Count == 0;
// 获取队列中元素数量
int count = queue.Count;
6.3 常见应用场景
6.3.1 广度优先搜索(BFS)
void BFS(TreeNode root)
{
if (root == null) return;
Queue<TreeNode> queue = new Queue<TreeNode>();
queue.Enqueue(root);
while (queue.Count > 0)
{
TreeNode node = queue.Dequeue();
Console.WriteLine(node.val);
if (node.left != null)
queue.Enqueue(node.left);
if (node.right != null)
queue.Enqueue(node.right);
}
}
树的结构:
1
/ \
2 3
/ \
4 5
遍历步骤:
1. 初始状态:队列 = [1]
2. 取出1,访问1,加入2和3:队列 = [2, 3]
3. 取出2,访问2,加入4和5:队列 = [3, 4, 5]
4. 取出3,访问3:队列 = [4, 5]
5. 取出4,访问4:队列 = [5]
6. 取出5,访问5:队列 = []
7. 队列空,遍历结束
输出顺序:1 -> 2 -> 3 -> 4 -> 5
6.3.2 任务调度
class Task
{
public string Name { get; set; }
public int Priority { get; set; }
}
class TaskScheduler
{
private Queue<Task> taskQueue = new Queue<Task>();
public void AddTask(Task task)
{
taskQueue.Enqueue(task);
}
public Task GetNextTask()
{
return taskQueue.Count > 0 ? taskQueue.Dequeue() : null;
}
}
6.3.3 消息队列
class Message
{
public string Content { get; set; }
public DateTime Timestamp { get; set; }
}
class MessageQueue
{
private Queue<Message> messageQueue = new Queue<Message>();
public void SendMessage(Message message)
{
messageQueue.Enqueue(message);
}
public Message ReceiveMessage()
{
return messageQueue.Count > 0 ? messageQueue.Dequeue() : null;
}
}
6.3.4 打印队列
class PrintJob
{
public string DocumentName { get; set; }
public int Pages { get; set; }
}
class Printer
{
private Queue<PrintJob> printQueue = new Queue<PrintJob>();
public void AddPrintJob(PrintJob job)
{
printQueue.Enqueue(job);
}
public void ProcessPrintJobs()
{
while (printQueue.Count > 0)
{
PrintJob job = printQueue.Dequeue();
Console.WriteLine($"打印文档:{job.DocumentName},页数:{job.Pages}");
}
}
}
6.4 性能特点
-
优点:
- 入队和出队操作都是O(1)时间复杂度
- 实现简单
- 内存使用效率高
- 适合处理需要按顺序处理的数据
-
缺点:
- 只能访问队首元素
- 不支持随机访问
- 空间大小固定(如果使用数组实现)
6.5 最佳实践
-
使用场景选择:
- 需要先进先出(FIFO)的场景
- 需要按顺序处理数据的场景
- 需要缓冲数据的场景
- 需要实现生产者-消费者模式的场景
-
实现建议:
- 使用C#内置的Queue类
- 注意队列的容量限制
- 处理队列空的情况
- 考虑使用泛型队列增加类型安全性
-
常见错误:
- 在空队列上调用Dequeue或Peek
- 队列溢出(如果使用固定大小数组)
- 忘记处理异常情况
-
优化技巧:
- 使用循环数组实现固定大小的队列
- 使用链表实现动态大小的队列
- 考虑使用优先级队列处理特殊需求
- 使用并发队列处理多线程场景
7. 双端队列(Deque)
7.1 基本概念
- 双端队列是一种可以在两端进行插入和删除操作的线性数据结构
- 支持在队首和队尾进行入队和出队操作
- 可以同时具有栈和队列的特性
- C#中可以使用
LinkedList<T>或自定义实现
7.2 基本实现
public class Deque<T>
{
private LinkedList<T> list = new LinkedList<T>();
// 在队首添加元素
public void AddFirst(T item)
{
list.AddFirst(item);
}
// 在队尾添加元素
public void AddLast(T item)
{
list.AddLast(item);
}
// 从队首移除元素
public T RemoveFirst()
{
if (list.Count == 0)
throw new InvalidOperationException("队列为空");
T item = list.First.Value;
list.RemoveFirst();
return item;
}
// 从队尾移除元素
public T RemoveLast()
{
if (list.Count == 0)
throw new InvalidOperationException("队列为空");
T item = list.Last.Value;
list.RemoveLast();
return item;
}
// 查看队首元素
public T PeekFirst()
{
if (list.Count == 0)
throw new InvalidOperationException("队列为空");
return list.First.Value;
}
// 查看队尾元素
public T PeekLast()
{
if (list.Count == 0)
throw new InvalidOperationException("队列为空");
return list.Last.Value;
}
// 获取元素数量
public int Count => list.Count;
// 检查是否为空
public bool IsEmpty => list.Count == 0;
}
7.3 常见应用场景
7.3.1 滑动窗口
public int[] MaxSlidingWindow(int[] nums, int k)
{
if (nums == null || nums.Length == 0 || k <= 0)
return new int[0];
int[] result = new int[nums.Length - k + 1];
Deque<int> deque = new Deque<int>();
for (int i = 0; i < nums.Length; i++)
{
// 移除超出窗口范围的元素
while (!deque.IsEmpty && deque.PeekFirst() <= i - k)
deque.RemoveFirst();
// 移除小于当前元素的元素
while (!deque.IsEmpty && nums[deque.PeekLast()] < nums[i])
deque.RemoveLast();
// 添加当前元素
deque.AddLast(i);
// 记录窗口最大值
if (i >= k - 1)
result[i - k + 1] = nums[deque.PeekFirst()];
}
return result;
}
7.3.2 回文检查
public bool IsPalindrome(string s)
{
Deque<char> deque = new Deque<char>();
// 将字符串转换为小写并移除非字母数字字符
foreach (char c in s.ToLower())
{
if (char.IsLetterOrDigit(c))
deque.AddLast(c);
}
// 从两端比较字符
while (deque.Count > 1)
{
if (deque.RemoveFirst() != deque.RemoveLast())
return false;
}
return true;
}
7.3.3 任务调度器
class Task
{
public string Name { get; set; }
public int Priority { get; set; }
}
class TaskScheduler
{
private Deque<Task> taskDeque = new Deque<Task>();
// 添加高优先级任务到队首
public void AddHighPriorityTask(Task task)
{
taskDeque.AddFirst(task);
}
// 添加普通任务到队尾
public void AddNormalTask(Task task)
{
taskDeque.AddLast(task);
}
// 获取下一个任务
public Task GetNextTask()
{
return taskDeque.Count > 0 ? taskDeque.RemoveFirst() : null;
}
}
7.4 性能特点
-
优点:
- 两端操作都是O(1)时间复杂度
- 灵活性高,可以同时作为栈和队列使用
- 适合需要双向操作的数据结构
-
缺点:
- 实现相对复杂
- 内存使用效率较低(如果使用链表实现)
- 随机访问效率低
7.5 最佳实践
-
使用场景选择:
- 需要双向操作的数据结构
- 滑动窗口问题
- 回文检查
- 需要同时具有栈和队列特性的场景
-
实现建议:
- 使用
LinkedList<T>实现 - 考虑使用数组实现固定大小的双端队列
- 注意边界条件的处理
- 使用泛型增加类型安全性
- 使用
-
常见错误:
- 在空队列上调用移除操作
- 忘记处理边界情况
- 内存泄漏(如果使用链表实现)
-
优化技巧:
- 使用循环数组实现固定大小的双端队列
- 考虑使用并发双端队列处理多线程场景
- 根据实际需求选择合适的实现方式
8. 堆(Heap)
8.1 基本概念
- 堆是一种特殊的完全二叉树
- 每个节点的值都大于等于(最大堆)或小于等于(最小堆)其子节点的值
- 常用于实现优先队列
- C#中可以使用
PriorityQueue<T>或自定义实现
8.2 基本实现
// 使用C#内置的PriorityQueue类
// 创建优先队列,TElement是元素类型,TPriority是优先级类型
PriorityQueue<string, int> priorityQueue = new PriorityQueue<string, int>();
// 添加元素(元素,优先级)
priorityQueue.Enqueue("任务1", 1); // 低优先级
priorityQueue.Enqueue("任务2", 2); // 中优先级
priorityQueue.Enqueue("任务3", 3); // 高优先级
// 获取并移除优先级最高的元素
string highestPriority = priorityQueue.Dequeue(); // 返回"任务3"
// 查看优先级最高的元素但不移除
string peek = priorityQueue.Peek(); // 返回"任务2"
// 检查队列是否为空
bool isEmpty = priorityQueue.Count == 0;
// 获取队列中元素数量
int count = priorityQueue.Count;
8.3 常见应用场景
8.3.1 优先队列
class Task
{
public string Name { get; set; }
public int Priority { get; set; }
}
class TaskScheduler
{
private PriorityQueue<Task, int> taskQueue = new PriorityQueue<Task, int>();
public void AddTask(Task task)
{
// 优先级数字越小,优先级越高
taskQueue.Enqueue(task, task.Priority);
}
public Task GetNextTask()
{
return taskQueue.Count > 0 ? taskQueue.Dequeue() : null;
}
}
8.3.2 合并K个有序链表
public ListNode MergeKLists(ListNode[] lists)
{
if (lists == null || lists.Length == 0)
return null;
// 创建优先队列,按节点值排序
var queue = new PriorityQueue<ListNode, int>();
// 将每个链表的头节点加入队列
foreach (var list in lists)
{
if (list != null)
queue.Enqueue(list, list.val);
}
ListNode dummy = new ListNode(0);
ListNode current = dummy;
// 不断从队列中取出最小节点
while (queue.Count > 0)
{
ListNode node = queue.Dequeue();
current.next = node;
current = current.next;
if (node.next != null)
queue.Enqueue(node.next, node.next.val);
}
return dummy.next;
}
8.3.3 数据流的中位数
class MedianFinder
{
// 存储较大的一半,最小堆
private PriorityQueue<int, int> minHeap = new PriorityQueue<int, int>();
// 存储较小的一半,最大堆(使用负数实现)
private PriorityQueue<int, int> maxHeap = new PriorityQueue<int, int>();
public void AddNum(int num)
{
// 将新数字加入最大堆
maxHeap.Enqueue(num, -num);
// 平衡两个堆
minHeap.Enqueue(maxHeap.Dequeue(), maxHeap.Peek());
// 确保最大堆的大小不小于最小堆
if (maxHeap.Count < minHeap.Count)
{
maxHeap.Enqueue(minHeap.Dequeue(), -minHeap.Peek());
}
}
public double FindMedian()
{
if (maxHeap.Count > minHeap.Count)
return maxHeap.Peek();
return (maxHeap.Peek() + minHeap.Peek()) / 2.0;
}
}
8.3.4 前K个高频元素
public int[] TopKFrequent(int[] nums, int k)
{
// 统计频率
Dictionary<int, int> frequency = new Dictionary<int, int>();
foreach (int num in nums)
{
if (!frequency.ContainsKey(num))
frequency[num] = 0;
frequency[num]++;
}
// 创建优先队列,按频率排序
var queue = new PriorityQueue<int, int>();
// 将元素加入队列
foreach (var pair in frequency)
{
queue.Enqueue(pair.Key, -pair.Value); // 使用负数实现最大堆
if (queue.Count > k)
queue.Dequeue();
}
// 获取结果
int[] result = new int[k];
for (int i = k - 1; i >= 0; i--)
{
result[i] = queue.Dequeue();
}
return result;
}
8.4 性能特点
-
优点:
- 插入和删除操作都是O(log n)时间复杂度
- 获取最大/最小值是O(1)时间复杂度
- 适合需要频繁获取最大/最小值的场景
- 空间效率高
-
缺点:
- 不支持随机访问
- 不支持快速查找
- 构建堆需要O(n)时间复杂度
8.5 最佳实践
-
使用场景选择:
- 需要频繁获取最大/最小值的场景
- 需要按优先级处理数据的场景
- 需要合并有序数据的场景
- 需要维护数据流中位数的场景
-
实现建议:
- 使用数组实现完全二叉树
- 注意堆的平衡性
- 合理选择最大堆或最小堆
- 考虑使用泛型增加类型安全性
-
常见错误:
- 忘记维护堆的性质
- 数组索引越界
- 比较函数实现错误
- 内存管理不当
-
优化技巧:
- 使用数组实现固定大小的堆
- 考虑使用并发堆处理多线程场景
- 根据实际需求选择合适的堆类型
- 优化比较函数的性能
9. 哈希表(Dictionary)
9.1 基本概念
- 哈希表是一种键值对(Key-Value)数据结构
- 通过哈希函数将键映射到存储位置
- 支持快速的查找、插入和删除操作
- C#中提供了多种哈希表实现:
-
Dictionary<TKey, TValue>:- 最常用的哈希表实现
- 线程不安全
- 支持泛型
Dictionary<string, int> dict = new Dictionary<string, int>(); -
Hashtable:- 非泛型版本
- 线程安全
- 键和值都是
object类型
Hashtable hashtable = new Hashtable(); hashtable["key"] = "value"; -
ConcurrentDictionary<TKey, TValue>:- 线程安全的泛型哈希表
- 适合多线程环境
ConcurrentDictionary<string, int> concurrentDict = new ConcurrentDictionary<string, int>(); -
SortedDictionary<TKey, TValue>:- 按键排序的哈希表
- 内部使用红黑树实现
SortedDictionary<string, int> sortedDict = new SortedDictionary<string, int>(); -
HashSet<T>:- 只存储键的哈希表
- 用于去重
HashSet<int> set = new HashSet<int>();
9.2 基本操作
// 创建字典
Dictionary<string, int> dict = new Dictionary<string, int>();
// 添加键值对
dict.Add("apple", 1);
dict["banana"] = 2; // 使用索引器添加
// 查找值
int value = dict["apple"]; // 使用索引器查找
bool exists = dict.TryGetValue("apple", out int value); // 安全查找
// 检查键是否存在
bool hasKey = dict.ContainsKey("apple");
// 删除键值对
dict.Remove("apple");
// 获取键值对数量
int count = dict.Count;
// 遍历字典
foreach (var pair in dict)
{
Console.WriteLine($"Key: {pair.Key}, Value: {pair.Value}");
}
// 只遍历键
foreach (var key in dict.Keys)
{
Console.WriteLine($"Key: {key}");
}
// 只遍历值
foreach (var value in dict.Values)
{
Console.WriteLine($"Value: {value}");
}
9.3 常见应用场景
9.3.1 计数统计
// 统计字符串中每个字符出现的次数
string str = "hello world";
Dictionary<char, int> charCount = new Dictionary<char, int>();
foreach (char c in str)
{
if (charCount.ContainsKey(c))
charCount[c]++;
else
charCount[c] = 1;
}
// 使用更简洁的写法
foreach (char c in str)
{
charCount[c] = charCount.GetValueOrDefault(c, 0) + 1;
}
9.3.2 缓存实现
class Cache<TKey, TValue>
{
private Dictionary<TKey, TValue> cache = new Dictionary<TKey, TValue>();
private int capacity;
public Cache(int capacity)
{
this.capacity = capacity;
}
public TValue Get(TKey key)
{
if (cache.TryGetValue(key, out TValue value))
return value;
return default;
}
public void Put(TKey key, TValue value)
{
if (cache.Count >= capacity)
{
// 移除最早添加的项
var firstKey = cache.Keys.First();
cache.Remove(firstKey);
}
cache[key] = value;
}
}
9.3.3 两数之和
public int[] TwoSum(int[] nums, int target)
{
Dictionary<int, int> dict = new Dictionary<int, int>();
for (int i = 0; i < nums.Length; i++)
{
int complement = target - nums[i];
if (dict.ContainsKey(complement))
{
return new int[] { dict[complement], i };
}
dict[nums[i]] = i;
}
return new int[0];
}
9.3.4 字符串分组
// 将具有相同字符的字符串分组
public IList<IList<string>> GroupAnagrams(string[] strs)
{
Dictionary<string, List<string>> groups = new Dictionary<string, List<string>>();
foreach (string str in strs)
{
// 将字符串排序作为键
char[] chars = str.ToCharArray();
Array.Sort(chars);
string key = new string(chars);
if (!groups.ContainsKey(key))
{
groups[key] = new List<string>();
}
groups[key].Add(str);
}
return groups.Values.ToList<IList<string>>();
}
9.3.5 最近最少使用(LRU)缓存
class LRUCache
{
private Dictionary<int, LinkedListNode<(int key, int value)>> cache;
private LinkedList<(int key, int value)> list;
private int capacity;
public LRUCache(int capacity)
{
this.capacity = capacity;
cache = new Dictionary<int, LinkedListNode<(int key, int value)>>();
list = new LinkedList<(int key, int value)>();
}
public int Get(int key)
{
if (!cache.ContainsKey(key))
return -1;
var node = cache[key];
list.Remove(node);
list.AddLast(node);
return node.Value.value;
}
public void Put(int key, int value)
{
if (cache.ContainsKey(key))
{
var node = cache[key];
list.Remove(node);
node.Value = (key, value);
list.AddLast(node);
}
else
{
if (cache.Count >= capacity)
{
var first = list.First;
cache.Remove(first.Value.key);
list.RemoveFirst();
}
var newNode = list.AddLast((key, value));
cache[key] = newNode;
}
}
}
9.4 性能特点
-
优点:
- 查找、插入、删除操作的平均时间复杂度为O(1)
- 支持任意类型的键和值
- 内存使用效率高
- 适合需要快速查找的场景
-
缺点:
- 不保证元素的顺序
- 哈希冲突可能影响性能
- 需要额外的内存空间
- 键必须是唯一的
9.5 最佳实践
-
使用场景选择:
- 需要快速查找的场景
- 需要计数统计的场景
- 需要缓存数据的场景
- 需要去重的场景
-
实现建议:
- 使用
TryGetValue代替ContainsKey和索引器 - 合理设置初始容量避免频繁扩容
- 注意键的唯一性
- 考虑使用
ConcurrentDictionary处理并发场景
- 使用
-
常见错误:
- 使用不存在的键访问值
- 忘记处理哈希冲突
- 使用可变对象作为键
- 并发访问时未使用线程安全的集合
-
优化技巧:
- 预分配容量减少扩容
- 使用合适的哈希函数
- 考虑使用
HashSet<T>处理只需要键的场景 - 使用
ConcurrentDictionary处理多线程场景
10. 集合(HashSet)
10.1 基本概念
- HashSet是一种不包含重复元素的集合
- 基于哈希表实现,提供O(1)的查找性能
- 不保证元素的顺序
- 适合需要快速查找和去重的场景
10.2 基本操作
// 创建HashSet
HashSet<int> set = new HashSet<int>();
// 添加元素
set.Add(1);
set.Add(2);
set.Add(3);
// 检查元素是否存在
bool contains = set.Contains(1); // 返回 true
// 删除元素
set.Remove(1);
// 获取元素数量
int count = set.Count;
// 遍历集合
foreach (int item in set)
{
Console.WriteLine(item);
}
// 清空集合
set.Clear();
10.3 常见应用场景
10.3.1 数组去重
int[] numbers = { 1, 2, 2, 3, 3, 4, 5, 5 };
HashSet<int> uniqueNumbers = new HashSet<int>(numbers);
// uniqueNumbers 包含 { 1, 2, 3, 4, 5 }
10.3.2 查找重复元素
public int[] FindDuplicates(int[] nums)
{
HashSet<int> seen = new HashSet<int>();
HashSet<int> duplicates = new HashSet<int>();
foreach (int num in nums)
{
if (!seen.Add(num)) // Add方法在元素已存在时返回false
{
duplicates.Add(num);
}
}
return duplicates.ToArray();
}
10.3.3 集合运算
HashSet<int> set1 = new HashSet<int> { 1, 2, 3, 4, 5 };
HashSet<int> set2 = new HashSet<int> { 4, 5, 6, 7, 8 };
// 并集
HashSet<int> union = new HashSet<int>(set1);
union.UnionWith(set2); // 结果: { 1, 2, 3, 4, 5, 6, 7, 8 }
// 交集
HashSet<int> intersection = new HashSet<int>(set1);
intersection.IntersectWith(set2); // 结果: { 4, 5 }
// 差集
HashSet<int> difference = new HashSet<int>(set1);
difference.ExceptWith(set2); // 结果: { 1, 2, 3 }
// 对称差集(只在一个集合中出现的元素)
HashSet<int> symmetricDifference = new HashSet<int>(set1);
symmetricDifference.SymmetricExceptWith(set2); // 结果: { 1, 2, 3, 6, 7, 8 }
10.3.4 判断集合关系
HashSet<int> set1 = new HashSet<int> { 1, 2, 3 };
HashSet<int> set2 = new HashSet<int> { 1, 2, 3, 4, 5 };
// 判断是否为子集
bool isSubset = set1.IsSubsetOf(set2); // 返回 true
// 判断是否为超集
bool isSuperset = set2.IsSupersetOf(set1); // 返回 true
// 判断是否重叠
bool overlaps = set1.Overlaps(set2); // 返回 true
// 判断是否相等
bool equals = set1.SetEquals(set2); // 返回 false
10.4 性能特点
-
优点:
- 查找、插入、删除操作的平均时间复杂度为O(1)
- 自动去重
- 内存使用效率高
- 支持集合运算
-
缺点:
- 不保证元素的顺序
- 哈希冲突可能影响性能
- 需要额外的内存空间
10.5 最佳实践
-
使用场景选择:
- 需要快速查找元素
- 需要去重
- 需要集合运算
- 不需要保持元素顺序
-
实现建议:
- 合理设置初始容量避免频繁扩容
- 使用
TryGetValue代替Contains - 考虑使用
ConcurrentHashSet处理并发场景
-
常见错误:
- 期望保持元素顺序
- 忘记处理哈希冲突
- 使用可变对象作为元素
- 并发访问时未使用线程安全的集合
-
优化技巧:
- 预分配容量减少扩容
- 使用合适的哈希函数
- 考虑使用
SortedSet<T>需要排序时 - 使用
ConcurrentHashSet处理多线程场景
11. 并查集(Disjoint Set)
11.1 基本概念
- 并查集是一种树形数据结构,用于处理不相交集合的合并及查询问题
- 支持两种操作:
- Find:查找元素所属的集合
- Union:合并两个集合
- 常用于解决连通性问题
11.2 基本实现
public class DisjointSet
{
private int[] parent; // 存储每个节点的父节点
private int[] rank; // 存储每个节点的秩(树的高度)
public DisjointSet(int size)
{
parent = new int[size];
rank = new int[size];
// 初始化,每个节点的父节点是自己
for (int i = 0; i < size; i++)
{
parent[i] = i;
rank[i] = 0;
}
}
// 查找元素所属的集合(路径压缩)
public int Find(int x)
{
if (parent[x] != x)
{
parent[x] = Find(parent[x]); // 路径压缩
}
return parent[x];
}
// 合并两个集合(按秩合并)
public void Union(int x, int y)
{
int rootX = Find(x);
int rootY = Find(y);
if (rootX == rootY) return;
// 按秩合并
if (rank[rootX] < rank[rootY])
{
parent[rootX] = rootY;
}
else if (rank[rootX] > rank[rootY])
{
parent[rootY] = rootX;
}
else
{
parent[rootY] = rootX;
rank[rootX]++;
}
}
// 检查两个元素是否属于同一集合
public bool IsConnected(int x, int y)
{
return Find(x) == Find(y);
}
}
11.3 常见应用场景
11.3.1 岛屿数量
public int NumIslands(char[][] grid)
{
if (grid == null || grid.Length == 0) return 0;
int rows = grid.Length;
int cols = grid[0].Length;
DisjointSet ds = new DisjointSet(rows * cols);
int count = 0;
// 遍历网格
for (int i = 0; i < rows; i++)
{
for (int j = 0; j < cols; j++)
{
if (grid[i][j] == '1')
{
count++;
// 检查上下左右
if (i > 0 && grid[i-1][j] == '1')
{
ds.Union(i * cols + j, (i-1) * cols + j);
count--;
}
if (j > 0 && grid[i][j-1] == '1')
{
ds.Union(i * cols + j, i * cols + (j-1));
count--;
}
}
}
}
return count;
}
11.3.2 朋友圈
public int FindCircleNum(int[][] M)
{
int n = M.Length;
DisjointSet ds = new DisjointSet(n);
// 遍历矩阵
for (int i = 0; i < n; i++)
{
for (int j = 0; j < n; j++)
{
if (M[i][j] == 1)
{
ds.Union(i, j);
}
}
}
// 统计不同的根节点数量
HashSet<int> roots = new HashSet<int>();
for (int i = 0; i < n; i++)
{
roots.Add(ds.Find(i));
}
return roots.Count;
}
11.3.3 冗余连接
public int[] FindRedundantConnection(int[][] edges)
{
int n = edges.Length;
DisjointSet ds = new DisjointSet(n + 1);
foreach (int[] edge in edges)
{
int x = edge[0];
int y = edge[1];
if (ds.IsConnected(x, y))
{
return edge;
}
ds.Union(x, y);
}
return new int[0];
}
11.3.4 最小生成树(Kruskal算法)
public int MinimumCost(int n, int[][] connections)
{
// 按权重排序
Array.Sort(connections, (a, b) => a[2].CompareTo(b[2]));
DisjointSet ds = new DisjointSet(n + 1);
int cost = 0;
int count = 0;
foreach (int[] connection in connections)
{
int x = connection[0];
int y = connection[1];
int weight = connection[2];
if (!ds.IsConnected(x, y))
{
ds.Union(x, y);
cost += weight;
count++;
}
if (count == n - 1)
{
return cost;
}
}
return -1;
}
11.4 性能特点
-
优点:
- 查找和合并操作的平均时间复杂度接近O(1)
- 空间复杂度为O(n)
- 适合处理大规模数据
- 实现简单,易于理解
-
缺点:
- 不支持集合的分离操作
- 不支持动态添加元素
- 需要预先知道元素数量
11.5 最佳实践
-
使用场景选择:
- 需要处理连通性问题
- 需要合并集合
- 需要判断元素是否属于同一集合
- 图论中的最小生成树问题
-
实现建议:
- 使用路径压缩优化查找操作
- 使用按秩合并优化合并操作
- 合理初始化数组大小
- 注意处理边界情况
-
常见错误:
- 忘记初始化父节点数组
- 没有使用路径压缩
- 没有使用按秩合并
- 数组越界
-
优化技巧:
- 使用路径压缩
- 使用按秩合并
- 使用哈希表存储父节点(当元素不是连续整数时)
- 考虑使用并发并查集处理多线程场景
12. 图(Graph)
12.1 基本概念
- 图是由顶点(Vertex)和边(Edge)组成的非线性数据结构
- 图可以分为有向图和无向图
- 边可以有权重(加权图)或无权重(无权图)
- 常见的表示方法:邻接矩阵和邻接表
12.2 基本实现
数组实现
12.2.1 邻接矩阵表示
bool[,] graph = new bool[4, 4];
graph[0, 1] = graph[1, 0] = true;
graph[0, 3] = graph[3, 0] = true;
graph[1, 2] = graph[2, 1] = true;
graph[2, 3] = graph[3, 2] = true;
12.2.2 邻接表表示
// 推荐写法
List<List<int>> graph = new List<List<int>>();
for (int i = 0; i < 4; i++)
graph.Add(new List<int>());
graph[0].Add(1); graph[0].Add(3);
graph[1].Add(0); graph[1].Add(2);
graph[2].Add(1); graph[2].Add(3);
graph[3].Add(0); graph[3].Add(2);
12.3 常见应用场景
12.3.1 深度优先搜索(DFS)
// 邻接表实现
void DFS_List(List<List<int>> graph, int v, bool[] visited)
{
visited[v] = true;
Console.WriteLine($"访问顶点: {v}");
foreach (int neighbor in graph[v])
{
if (!visited[neighbor])
DFS_List(graph, neighbor, visited);
}
}
// 邻接矩阵实现
void DFS_Matrix(bool[,] graph, int v, bool[] visited)
{
visited[v] = true;
Console.WriteLine($"访问顶点: {v}");
int n = graph.GetLength(0);
for (int u = 0; u < n; u++)
{
if (graph[v, u] && !visited[u])
DFS_Matrix(graph, u, visited);
}
}
12.3.2 广度优先搜索(BFS)
// 邻接表实现
void BFS_List(List<List<int>> graph, int start)
{
int n = graph.Count;
bool[] visited = new bool[n];
Queue<int> queue = new Queue<int>();
visited[start] = true;
queue.Enqueue(start);
while (queue.Count > 0)
{
int v = queue.Dequeue();
Console.WriteLine($"访问顶点: {v}");
foreach (int neighbor in graph[v])
{
if (!visited[neighbor])
{
visited[neighbor] = true;
queue.Enqueue(neighbor);
}
}
}
}
// 邻接矩阵实现
void BFS_Matrix(bool[,] graph, int start)
{
int n = graph.GetLength(0);
bool[] visited = new bool[n];
Queue<int> queue = new Queue<int>();
visited[start] = true;
queue.Enqueue(start);
while (queue.Count > 0)
{
int v = queue.Dequeue();
Console.WriteLine($"访问顶点: {v}");
for (int u = 0; u < n; u++)
{
if (graph[v, u] && !visited[u])
{
visited[u] = true;
queue.Enqueue(u);
}
}
}
}
12.3.3 Dijkstra最短路径算法(加权无向图)
// 邻接表实现(需要权重信息,示例为int权重)
int[] Dijkstra_List(List<List<(int to, int weight)>> graph, int start)
{
int n = graph.Count;
int[] dist = new int[n];
bool[] visited = new bool[n];
for (int i = 0; i < n; i++) dist[i] = int.MaxValue;
dist[start] = 0;
var pq = new PriorityQueue<int, int>();
pq.Enqueue(start, 0);
while (pq.Count > 0)
{
int u = pq.Dequeue();
if (visited[u]) continue;
visited[u] = true;
foreach (var (v, w) in graph[u])
{
if (!visited[v] && dist[u] + w < dist[v])
{
dist[v] = dist[u] + w;
pq.Enqueue(v, dist[v]);
}
}
}
return dist;
}
// 邻接矩阵实现(int权重,0表示无边)
int[] Dijkstra_Matrix(int[,] graph, int start)
{
int n = graph.GetLength(0);
int[] dist = new int[n];
bool[] visited = new bool[n];
for (int i = 0; i < n; i++) dist[i] = int.MaxValue;
dist[start] = 0;
for (int i = 0; i < n; i++)
{
int u = -1, min = int.MaxValue;
for (int j = 0; j < n; j++)
{
if (!visited[j] && dist[j] < min)
{
min = dist[j];
u = j;
}
}
if (u == -1) break;
visited[u] = true;
for (int v = 0; v < n; v++)
{
if (graph[u, v] > 0 && dist[u] + graph[u, v] < dist[v])
{
dist[v] = dist[u] + graph[u, v];
}
}
}
return dist;
}
12.3.4 拓扑排序(有向无环图)
// 邻接表实现
List<int> TopoSort_List(List<List<int>> graph)
{
int n = graph.Count;
int[] indegree = new int[n];
foreach (var edges in graph)
foreach (var v in edges)
indegree[v]++;
Queue<int> queue = new Queue<int>();
for (int i = 0; i < n; i++)
if (indegree[i] == 0) queue.Enqueue(i);
List<int> result = new List<int>();
while (queue.Count > 0)
{
int u = queue.Dequeue();
result.Add(u);
foreach (int v in graph[u])
{
if (--indegree[v] == 0)
queue.Enqueue(v);
}
}
return result;
}
// 邻接矩阵实现
List<int> TopoSort_Matrix(bool[,] graph)
{
int n = graph.GetLength(0);
int[] indegree = new int[n];
for (int u = 0; u < n; u++)
for (int v = 0; v < n; v++)
if (graph[u, v]) indegree[v]++;
Queue<int> queue = new Queue<int>();
for (int i = 0; i < n; i++)
if (indegree[i] == 0) queue.Enqueue(i);
List<int> result = new List<int>();
while (queue.Count > 0)
{
int u = queue.Dequeue();
result.Add(u);
for (int v = 0; v < n; v++)
{
if (graph[u, v] && --indegree[v] == 0)
queue.Enqueue(v);
}
}
return result;
}
12.4 性能特点
-
优点:
- 邻接矩阵:
- 查找边的时间复杂度为O(1)
- 适合稠密图
- 邻接表:
- 空间效率高
- 适合稀疏图
- 遍历邻接点效率高
- 邻接矩阵:
-
缺点:
- 邻接矩阵:
- 空间复杂度为O(V²)
- 不适合稀疏图
- 邻接表:
- 查找边的时间复杂度为O(E)
- 删除边操作较慢
- 邻接矩阵:
12.5 最佳实践
-
使用场景选择:
- 邻接矩阵:
- 图较稠密
- 需要频繁查找边
- 顶点数量较少
- 邻接表:
- 图较稀疏
- 需要频繁遍历邻接点
- 顶点数量较多
- 邻接矩阵:
-
实现建议:
- 根据实际需求选择合适的表示方法
- 考虑使用优先队列优化Dijkstra算法
- 注意处理有向图和无向图的区别
- 合理处理权重和距离的计算
-
常见错误:
- 忘记处理自环
- 混淆有向图和无向图
- 权重计算错误
- 访问已访问的顶点
-
优化技巧:
- 使用优先队列优化最短路径算法
- 使用并查集优化最小生成树算法
- 使用动态规划优化某些路径问题
- 考虑使用并行算法处理大规模图

浙公网安备 33010602011771号