算法入门篇(五)之 树的应用

目录

1.树和二叉树

1.1树(Tree)

1.1.1 特点

1.1.2 使用场景

1.1.3 示例

1.2二叉树(Binary Tree)

1.2.1 特点

1.2.2 使用场景

1.2.3 示例

2.二叉树遍历

2.1 先序遍历、中序遍历、后序遍历、层次遍历

2.1.1 先序遍历(Preorder Traversal)

2. 1.2 中序遍历(Inorder Traversal)

2.1.3 后序遍历(Postorder Traversal)

2.1.4 层次遍历(Level-Order Traversal)

2.2遍历序列还原树

2.2.1 介绍

2.2.2 思路

2.2.3 示例

2.3 P1305、UVA536、UVA548

2.3.1 P1305

2.3.2 UVA536

2.3.3 UVA548

3.哈夫曼树

3.1 哈夫曼编码

3.1.1 定义与背景

3.1.2 基本原理

3.1.3 编码过程

3.1.4 解码过程

3.1.5 应用场景

3.1.6 总结

3.2 可变基哈夫曼编码

3.2.1 定义与特点

3.2.2 构建过程

3.2.3 编码与解码

3.2.4 应用场景

3.2.5 注意事项

3.3 POJ3253、POJ1521、UVA12676、UVA240

3.3.1 POJ 3253: Fence Repair (Planks)

3.3.2 POJ 1521: Pie Progress

3.3.3 UVA 12676: "Grocery Store"

3.3.4 UVA 240: "Returning Home"

 


1.树和二叉树

树(Tree)和二叉树(Binary Tree)是计算机科学中非常基础且重要的数据结构。它们用于存储和操作数据项,以反映层级或分支关系。下面,我将对这两种数据结构进行详细说明,并在需要时提供C#语言的简单实现示例。

1.1树(Tree)

树是一种递归定义的数据结构,包含一个根节点和零个或多个子树,每个子树本身也是一棵树。树中的每个节点都包含数据部分和指向其子节点的链接(也称为边或分支)。

1.1.1 特点

  • 每个节点可以有零个或多个子节点。

  • 没有父节点的节点称为根节点。

  • 每个非根节点有且仅有一个父节点。

  • 除了根节点外,每个节点都有唯一的路径通向根节点。

  • 如果存在一条从节点P到节点Q的路径,且P和Q不相同,则称P是Q的祖先,Q是P的后代。

1.1.2 使用场景

  • 文件系统:文件和目录的层级关系可以用树来表示。

  • 组织结构图:公司的部门、职位等层级关系。

  • 决策树:在机器学习和数据挖掘中用于分类和决策。

  • 表达式树:在编译器设计中用于表示数学表达式。

1.1.3 示例

由于树的实现依赖于具体的应用场景,这里不直接给出具体的C#代码,但你可以通过定义节点类(包含数据和子节点列表)来开始实现一个基本的树结构。

Python代码示例:

class TreeNode:
    def __init__(self, value):
        self.value = value
        self.children = []

    def add_child(self, child_node):
        self.children.append(child_node)

# 使用示例
root = TreeNode("root")
child1 = TreeNode("child1")
child2 = TreeNode("child2")
root.add_child(child1)
root.add_child(child2)

# 添加子节点的子节点
child1_1 = TreeNode("child1_1")
child1.add_child(child1_1)

# 打印树(简单遍历)
def print_tree(node, level=0):
    print('  ' * level + '->', node.value)
    for child in node.children:
        print_tree(child, level + 1)

print_tree(root)

1.2二叉树(Binary Tree)

二叉树是树的一种特殊形式,其中每个节点最多有两个子节点,通常称为左子节点和右子节点。

1.2.1 特点

  • 每个节点最多有两个子节点。

  • 节点的子节点有左右之分,且顺序不能颠倒。

  • 没有子节点的节点称为叶子节点。

  • 只有一个子节点的节点称为单支节点。

  • 深度(Depth)为根节点到最远叶子节点的最长路径上的节点数。

  • 高度(Height)为从该节点到最远叶子节点的最长路径上的边数。

1.2.2 使用场景

  • 搜索树:如二叉搜索树(BST),用于高效地查找、插入和删除数据。

  • 堆:一种特殊的完全二叉树,用于实现优先队列。

  • 表达式树:在编译器中用于解析和计算表达式的值。

1.2.3 示例

C# 代码示例:

public class TreeNode
{
    public int Val { get; set; }
    public TreeNode Left { get; set; }
    public TreeNode Right { get; set; }

    public TreeNode(int val = 0, TreeNode left = null, TreeNode right = null)
    {
        Val = val;
        Left = left;
        Right = right;
    }
}

// 使用TreeNode类可以构建二叉树,例如:
TreeNode root = new TreeNode(1);
root.Left = new TreeNode(2);
root.Right = new TreeNode(3);
root.Left.Left = new TreeNode(4);
root.Left.Right = new TreeNode(5);

这个示例中,我们定义了一个TreeNode类来表示二叉树的节点,并通过这个类构建了一个简单的二叉树。每个节点包含一个整数值Val和两个指向其子节点的引用LeftRight。通过这种方式,我们可以递归地构建更复杂的二叉树结构。

Python代码示例(二叉搜索树):

class BinaryTreeNode:
    def __init__(self, value):
        self.value = value
        self.left = None
        self.right = None

    def insert(self, value):
        if value < self.value:
            if self.left is None:
                self.left = BinaryTreeNode(value)
            else:
                self.left.insert(value)
        else:
            if self.right is None:
                self.right = BinaryTreeNode(value)
            else:
                self.right.insert(value)

# 使用示例
bst = BinaryTreeNode(10)
bst.insert(5)
bst.insert(15)
bst.insert(0)
bst.insert(7)

# 简单的中序遍历打印BST
def inorder_traversal(node):
    if node:
        inorder_traversal(node.left)
        print(node.value, end=' ')
        inorder_traversal(node.right)

inorder_traversal(bst)  # 输出应该是按升序排列的节点值

2.二叉树遍历

2.1 先序遍历、中序遍历、后序遍历、层次遍历

在树形结构中,尤其是二叉树,有四种常见的遍历方式:先序遍历(Preorder Traversal)、中序遍历(Inorder Traversal)、后序遍历(Postorder Traversal)和层次遍历(Level-Order Traversal,也称为广度优先遍历Breadth-First Search, BFS)。下面我将分别介绍这四种遍历方式,并给出在Python中的示例代码。

2.1.1 先序遍历(Preorder Traversal)

先序遍历首先访问根节点,然后遍历左子树,最后遍历右子树。

Python代码示例:

class TreeNode:
    def __init__(self, x):
        self.val = x
        self.left = None
        self.right = None

def preorderTraversal(root):
    if root is None:
        return []
    result = []
    stack = [root]
    while stack:
        node = stack.pop()
        result.append(node.val)
        if node.right:
            stack.append(node.right)
        if node.left:
            stack.append(node.left)
    return result

2. 1.2 中序遍历(Inorder Traversal)

中序遍历首先遍历左子树,然后访问根节点,最后遍历右子树。

Python 示例代码

def inorderTraversal(root):
    if root is None:
        return []
    result = []
    stack = []
    node = root
    while stack or node:
        while node:
            stack.append(node)
            node = node.left
        node = stack.pop()
        result.append(node.val)
        node = node.right
    return result

2.1.3 后序遍历(Postorder Traversal)

后序遍历首先遍历左子树,然后遍历右子树,最后访问根节点。

Python 示例代码(使用两个栈):

def postorderTraversal(root):
    if root is None:
        return []
    result, stack, output = [], [root], []
    while stack:
        node = stack.pop()
        output.append(node)
        if node.left:
            stack.append(node.left)
        if node.right:
            stack.append(node.right)
    while output:
        result.append(output.pop().val)
    return result

2.1.4 层次遍历(Level-Order Traversal)

层次遍历从根节点开始,从上到下,从左到右遍历树的每个节点。

Python 示例代码

from collections import deque

def levelOrderTraversal(root):
    if root is None:
        return []
    queue = deque([root])
    result = []
    while queue:
        level_size = len(queue)
        level_nodes = []
        for _ in range(level_size):
            node = queue.popleft()
            level_nodes.append(node.val)
            if node.left:
                queue.append(node.left)
            if node.right:
                queue.append(node.right)
        result.append(level_nodes)
    return result

2.2遍历序列还原树

遍历序列还原树是一个在给定树的遍历序列(通常是先序遍历、中序遍历或后序遍历)的基础上,尝试重建原始树结构的过程。由于后序遍历单独使用无法唯一确定一棵二叉树(除非树的所有节点值都是唯一的),这里我们主要讨论使用先序遍历和中序遍历序列来还原二叉树的情况。

2.2.1 介绍

  • 先序遍历:首先访问根节点,然后遍历左子树,最后遍历右子树。

  • 中序遍历:首先遍历左子树,然后访问根节点,最后遍历右子树。

由于先序遍历的第一个元素总是根节点,而中序遍历将左子树和右子树分开,我们可以利用这一特性来递归地重建二叉树。

2.2.2 思路

  1. 从先序遍历序列中取出第一个元素作为根节点。

  2. 在中序遍历序列中找到这个根节点,它将中序遍历序列分为左子树和右子树两部分。

  3. 递归地对左子树和右子树的中序遍历和先序遍历序列进行上述操作。

2.2.3 示例

class TreeNode:
    def __init__(self, val=0, left=None, right=None):
        self.val = val
        self.left = left
        self.right = right

def buildTree(preorder, inorder):
    if not preorder or not inorder:
        return None
    
    # 先序遍历的第一个元素为根节点
    root_val = preorder[0]
    root = TreeNode(root_val)
    
    # 在中序遍历中找到根节点的索引
    mid_index = inorder.index(root_val)
    
    # 切割中序遍历数组,得到左子树和右子树的中序遍历
    inorder_left = inorder[:mid_index]
    inorder_right = inorder[mid_index+1:]
    
    # 根据中序遍历的切割点,确定先序遍历中左子树和右子树的节点数
    # 进而切割先序遍历数组
    preorder_left = preorder[1:mid_index+1]
    preorder_right = preorder[mid_index+1:]
    
    # 递归构建左子树和右子树
    root.left = buildTree(preorder_left, inorder_left)
    root.right = buildTree(preorder_right, inorder_right)
    
    return root

# 示例
preorder = [3, 9, 20, 15, 7]
inorder = [9, 3, 15, 20, 7]
root = buildTree(preorder, inorder)

# 遍历验证(例如,中序遍历)
def inorderTraversal(root):
    if root:
        inorderTraversal(root.left)
        print(root.val, end=' ')
        inorderTraversal(root.right)

inorderTraversal(root)  # 输出应该是 9 3 15 20 7

注意:这个代码示例中,我们假设树中不存在重复的元素,因为如果有重复元素,中序遍历的切割点可能不是唯一的,这将导致无法唯一确定一棵二叉树。如果树中允许有重复元素,那么可能需要额外的信息(如节点索引、节点父子关系等)来唯一确定树的结构。

此外,这段代码使用了递归方法来构建树,它可能不适用于非常大的树,因为递归会占用大量的调用栈空间。对于大型数据集,可能需要考虑使用迭代方法或优化递归算法。

2.3 P1305、UVA536、UVA548

P1305、UVA536、和UVA548 是三个不同的编程竞赛或在线评测系统中的题目编号,它们分别来自不同的平台或竞赛。根据题目编号和常见的编程竞赛题目类型给出一些一般性的建议和解题思路。

2.3.1 P1305

题目可能类型:这通常是一个来自中国某在线评测系统(如洛谷)的题目编号。由于没有具体的题目名称或描述,我们无法确定具体的题目类型,但可能是数据结构、算法设计、字符串处理、图论等常见的编程竞赛题目。

解题思路

  • 首先,查看题目描述,理解题目要求和输入输出格式。

  • 分析题目所给数据规模,选择合适的算法和数据结构。

  • 设计算法,可能需要使用到递归、分治、贪心、动态规划、搜索(DFS/BFS)等策略。

  • 编写代码,注意边界条件和特殊情况的处理。

  • 调试并测试代码,确保在各种情况下都能得到正确的结果。

2.3.2 UVA536

题目类型:UVA(University of Valladolid Algorithm Repository)是一个在线的编程竞赛和算法练习平台,UVA536可能是一个具体的题目编号。由于UVA上的题目类型非常广泛,从简单的编程练习到复杂的算法设计都有,所以无法直接确定UVA536的具体类型。

解题思路

  • 类似于P1305,首先查看题目描述,理解题目要求和输入输出格式。

  • 分析题目难度和数据规模,选择合适的算法和数据结构。

  • 注意UVA平台可能有一些特殊的输入输出格式要求,如多组输入、特定格式的输出等。

  • 编写代码并调试,确保在各种情况下都能通过UVA的评测系统。

2.3.3 UVA548

题目类型:同样,UVA548也是UVA平台上的一个题目编号,其类型和难度无法直接确定。

解题思路

  • 遵循与UVA536相同的解题步骤。

  • 由于UVA平台上的题目可能较为经典或具有挑战性,因此可能需要花费更多的时间和精力来分析和解决问题。

  • 利用在线资源,如论坛、博客等,查找其他人的解题思路和代码实现,这有助于拓宽思路并快速找到问题的解决方案。

总之,对于这三个题目编号,最重要的是仔细阅读题目描述,理解题目要求,并根据题目类型和难度选择合适的算法和数据结构来解决问题。同时,注意调试和测试代码,确保在各种情况下都能得到正确的结果。

3.哈夫曼树

3.1 哈夫曼编码

哈夫曼编码(Huffman Coding),又称霍夫曼编码,是一种重要的编码方式,尤其在数据压缩领域有着广泛的应用。以下是对哈夫曼编码的详细介绍:

3.1.1 定义与背景

  • 定义:哈夫曼编码是一种可变字长编码(VLC),它根据字符出现的概率来构造异字头的平均长度最短的码字,从而实现对数据的有效压缩。

  • 背景:该编码方法由David A. Huffman在1952年提出,基于香农(Shannon)在1948年和范若(Fano)在1949年阐述的编码思想。

3.1.2 基本原理

  • 构建哈夫曼树:哈夫曼编码的核心是构建一棵哈夫曼树(也称为最优二叉树)。在哈夫曼树中,出现频率越高的字符离根节点越近,出现频率越低的字符则离根节点越远。

  • 编码规则:通过自底向上遍历哈夫曼树,可以得到每个字符对应的哈夫曼编码。通常,左子树编码为0,右子树编码为1。

3.1.3 编码过程

  1. 统计字符频率:首先,对需要编码的数据进行扫描,统计每个字符出现的频率。

  2. 构建哈夫曼树:根据字符频率构建哈夫曼树,具体过程包括多次合并频率最小的两个节点,并将它们作为新节点的子节点,新节点的频率为两个子节点频率之和。

  3. 生成编码表:遍历哈夫曼树,为每个字符生成对应的哈夫曼编码,并存储在编码表中。

  4. 数据编码:使用编码表对原始数据进行编码,得到压缩后的数据。

3.1.4 解码过程

  • 解码是编码的逆过程,通过哈夫曼树和编码表可以恢复出原始数据。

  • 从压缩数据的第一个位开始,根据哈夫曼树的路径(0或1)找到对应的字符,直到遍历完所有数据。

3.1.5 应用场景

  • 数据压缩:哈夫曼编码被广泛用于无损数据压缩领域,特别是在图像、音频和视频压缩中。通过变长编码,出现频率高的符号被赋予较短的编码,从而减少了存储所需的比特数。

  • 通信系统:在通信领域,使用哈夫曼编码可以减少传输数据的位数,降低传输成本。这在无线通信、互联网通信等场景中尤为重要。

  • 文件存储:哈夫曼编码也常用于文件存储中,以减小文件的体积,提高存储效率。

  • 数据加密:在一些加密算法中,哈夫曼编码可用于将加密后的数据进行压缩,从而提高数据传输的效率。

  • 字典压缩:对于字典等需要频繁查询和存储的数据结构,哈夫曼编码可以实现高效的压缩和检索。

3.1.6 总结

哈夫曼编码是一种基于字符出现频率的编码方式,通过构建哈夫曼树并生成编码表来实现数据的压缩和解压缩。该编码方式在数据压缩、通信系统、文件存储等领域具有广泛的应用价值。

3.2 可变基哈夫曼编码

可变基哈夫曼编码(Variable Radix Huffman Encoding)是哈夫曼编码的一种扩展形式,它在传统的二叉树基础上,将二叉树扩展为R叉树(R > 2),从而提供了更多的灵活性和编码效率。以下是对可变基哈夫曼编码的详细解释:

3.2.1 定义与特点

  • 定义:可变基哈夫曼编码是一种基于R叉树的哈夫曼编码方法,其中R是大于2的整数,表示每个节点可以有R个子节点。

  • 特点

    • 灵活性高:通过调整R的值,可以根据具体的应用场景和数据特性来优化编码效率。
    • 编码效率高:在某些情况下,相比于传统的二叉哈夫曼树,R叉树能够生成更短的平均编码长度。

3.2.2 构建过程

可变基哈夫曼编码的构建过程与传统的哈夫曼编码类似,但需要考虑R叉树的特点。以下是一个简化的构建过程:

  1. 统计字符频率:首先,对需要编码的数据进行扫描,统计每个字符出现的频率。

  2. 初始化节点:将每个字符及其频率作为叶子节点,初始化一个R叉树的节点集合。

  3. 合并节点

    • 从节点集合中选出R-1个频率最小的节点(如果节点总数不足R-1,则全部选取)。
    • 创建一个新的内部节点,将这R-1个节点作为其子节点,新节点的频率为这些子节点频率之和。
    • 将新节点加入节点集合,并移除原来的R-1个节点。
  4. 重复合并:重复上述合并过程,直到节点集合中只剩下一个节点,即哈夫曼树的根节点。

  5. 生成编码表:从根节点开始,遍历哈夫曼树,为每个叶子节点(即字符)生成对应的编码。在遍历过程中,根据路径上的分支(0到R-1)来生成编码。

3.2.3 编码与解码

  • 编码:使用生成的编码表对原始数据进行编码,得到压缩后的数据。

  • 解码:解码是编码的逆过程,通过遍历哈夫曼树和读取编码来恢复原始数据。

3.2.4 应用场景

可变基哈夫曼编码适用于需要高效数据压缩的场景,特别是在字符集较大或字符频率分布不均匀的情况下。通过调整R的值,可以针对不同的应用场景进行优化,以达到更好的压缩效果。

3.2.5 注意事项

  • 在实际应用中,需要根据具体的数据特性和压缩需求来选择合适的R值。

  • 由于R叉树的构建过程相对复杂,因此在实现时需要注意算法的优化和效率。

  • 在进行编码和解码时,需要确保编码表的一致性,以避免数据错误。

总之,可变基哈夫曼编码是一种灵活的编码方法,通过调整R叉树的结构来优化编码效率。它在数据压缩领域具有广泛的应用前景。

3.3 POJ3253、POJ1521、UVA12676、UVA240

3.3.1 POJ 3253: Fence Repair (Planks)

问题总结: 你有 N块不同长度的木板。你需要通过将这些木板全部合并成一块来修理围栏。每次合并两块木板的费用等于它们长度之和。目标是找到合并所有木板的最小费用。

方案:

  1. 使用最小堆始终优先合并最短的两块木板.
  2. 将合并后的木板重新放入堆中.
  3. 重复此过程直到只剩下一块木板.

代码示例 (Python):

import heapq

def fence_repair(planks):
    heapq.heapify(planks)
    total_cost = 0

    while len(planks) > 1:
        first = heapq.heappop(planks)
        second = heapq.heappop(planks)
        cost = first + second
        total_cost += cost
        heapq.heappush(planks, cost)

    return total_cost

# Example Usage
planks = [8, 5, 8]
print(fence_repair(planks))  # Output: 34

3.3.2 POJ 1521: Pie Progress

问题总结: 你有 n 个饼和 m 天。每一天你都必须买一个饼,但饼每天会变得更贵。找到在 m天内购买 n个饼的最低总费用.

方案:

  1. 使用优先队列来跟踪下一个饼的最低成本.
  2. 每天,取出最低成本的饼,购买它,并将当天下一个饼的成本(如果有的话)放入队列中.

代码示例 (Python):

import heapq

def pie_progress(costs, n, m):
    min_heap = []
    for day in range(m):
        heapq.heappush(min_heap, (costs[day][0], day, 0))

    total_cost = 0
    for _ in range(n):
        cost, day, pie_index = heapq.heappop(min_heap)
        total_cost += cost
        if pie_index + 1 < len(costs[day]):
            next_cost = costs[day][pie_index + 1]
            heapq.heappush(min_heap, (next_cost, day, pie_index + 1))

    return total_cost

# Example Usage
costs = [
    [3, 5, 7],
    [2, 4, 6],
    [1, 8, 9]
]
print(pie_progress(costs, 3, 3))  # Example usage

3.3.3 UVA 12676: "Grocery Store"

问题总结: 给定一个物品价格列表和一个预算。你需要在不超出预算的情况下找到你可以购买的最多物品数量。

Approach:

  1. 将物品价格列表排序.
  2. 遍历排序后的列表并累加价格,直到超出预算为止

代码示例 (Python):

def max_items(prices, budget):
    prices.sort()
    total_items = 0
    total_cost = 0

    for price in prices:
        if total_cost + price <= budget:
            total_cost += price
            total_items += 1
        else:
            break

    return total_items

# Example Usage
prices = [5, 10, 3, 7, 2]
budget = 15
print(max_items(prices, budget))  # Output: 3

3.3.4 UVA 240: "Returning Home"

问题总结:给定一个网格,你需要找到从起点到终点的最短路径,并避开障碍物。

方案:

  1. 使用广度优先搜索(BFS)在无权网格中找到最短路径.

代码示例 (Python):

from collections import deque

def bfs_shortest_path(grid, start, end):
    rows, cols = len(grid), len(grid[0])
    directions = [(0, 1), (1, 0), (0, -1), (-1, 0)]
    queue = deque([start])
    visited = set()
    visited.add(start)
    distance = {start: 0}

    while queue:
        x, y = queue.popleft()
        if (x, y) == end:
            return distance[(x, y)]
        for dx, dy in directions:
            nx, ny = x + dx, y + dy
            if 0 <= nx < rows and 0 <= ny < cols and grid[nx][ny] != '#' and (nx, ny) not in visited:
                queue.append((nx, ny))
                visited.add((nx, ny))
                distance[(nx, ny)] = distance[(x, y)] + 1

    return -1  # If no path is found

# Example Usage
grid = [
    ['S', '.', '.', '#', '.', '.', '.'],
    ['.', '#', '.', '.', '.', '#', '.'],
    ['.', '#', '.', '.', '.', '.', '.'],
    ['.', '.', '#', '#', '.', '.', '.'],
    ['#', '.', '#', 'E', '.', '#', '.']
]
start = (0, 0)  # Starting position 'S'
end = (4, 3)    # Ending position 'E'
print(bfs_shortest_path(grid, start, end))  # Example usage

每个问题都涉及不同的算法方法,从贪心算法和优先队列到排序和广度优先搜索。请务必根据具体需求调整输入和测试用例。

 

  • 16
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

战族狼魂

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值