浅聊哨兵思想及其在算法问题中的应用

简介: 前几日有个学妹来问一道题(回文链表),我给了一段示例代码,介绍了一下哨兵思想,但学妹似乎还有疑问。谷歌找了相关资料,但资料相对比较零散,索性自己整理了相关的资料,聊聊哨兵思想及其应用。

2 编辑

网络异常,图片无法展示
|

前言

前几日有个学妹来问一道题(回文链表),我给了一段示例代码,介绍了一下哨兵思想,但学妹似乎还有疑问。谷歌找了相关资料,但资料相对比较零散,索性自己整理了相关的资料,聊聊哨兵思想及其应用。

网络异常,图片无法展示
|

哨兵思想

起源

哨兵思想的起源已经无从得知,但它最早可以追溯到上世纪 60 年代。在那时候,人们已经开始使用哨兵(sentinel)来简化链表的实现。哨兵是指在链表中插入一个节点,用于标记链表的开头或结尾。

定义

哨兵思想是指在算法中使用一个特殊值来检测或标记某些条件的发生,它的目的是为了简化代码,并使其更容易理解,常常用于在循环中优化边界条件的判断。

假设你是一名守卫,你的任务是在一个城市的城门处保卫城市,防止有人进入或离开城市。你的城市有很多城门,你需要站在城门处,监视每个人是否进入或离开城市。

网络异常,图片无法展示
|
为了方便起见,你决定设置一个哨兵,来帮助你监视所有城门。哨兵可以在城门处站岗,当有人进入或离开城市时,哨兵会立即告诉你。这样,你就不必在每个城门处都站岗,而只需要在哨兵处站岗即可。

在这个例子中,哨兵就是在链表中的哨兵,它的作用是帮助监视所有城门(也就是链表中的所有节点)。通过使用哨兵,你(也就是程序)可以更方便地监视所有城门,而不必在每个城门处都站岗(也就是在每个节点处进行特殊处理)。 例如,在某些数据结构中,可能需要使用哨兵值来标识链表的结尾或空值。在这种情况下,哨兵值可以用来避免在每次操作中进行空值检查。这样可以提高程序的效率,同时也可以简化代码的实现。

哨兵思想也可以用于其他数据结构中,例如数组和树结构。在这些情况下,哨兵值可以用来标识数组边界或树中的特殊节点。

以下是利用哨兵思想解决问题的一般思路

image.png

举几个例子

例一:数组

在查找某个值在数组中的位置时,我们常常需要特殊判断数组的第一个元素。如果我们使用哨兵思想,就可以把这些特殊判断都省略掉.

在使用哨兵思想时,通常需要将数组的第一个元素设置为哨兵值。然后在遍历数组时,逐个与哨兵值进行比较,找到了就结束循环。这样,就可以省略掉对数组第一个元素的特殊判断。

示例代码

def search(arr, x):
    temp = arr[0]  # 保存数组的第一个元素
    arr[0] = x  # 将数组的第一个元素作为哨兵值,设置为要查找的值
    i = len(arr) - 1
    while arr[i] != x:
        i -= 1
    arr[0] = temp  # 恢复数组的第一个元素
    return i if arr[i] == x else -1  # 返回哨兵值的下标,如果找不到则返回
复制代码

复杂度分析

假设我们要查找的值在数组的最后一个位置,那么算法需要遍历整个数组才能找到这个值。因此,在最坏情况下,算法的时间复杂度为 O(n),其中 n 为数组的长度。

因为算法只使用了常数级别的额外空间,所以空间复杂度为 O(1)。

这个算法的时间复杂度是比较优秀的,但如果要对数组进行多次查找操作,那么就有可能需要使用其他算法(如哈希表等)来提升效率。为避免文章冗长,这里笔者不再展示代码,相关实现

解题思维导图

image.png

例二:链表

在链表中,哨兵是一个空的节点,它的作用是方便对链表的头部或尾部进行操作。

举个例子,使用哨兵可以避免在头部插入节点时,需要特判链表是否为空的情况。可以将哨兵节点设为链表的第一个节点,并让它的 next 指针指向真正的第一个节点。然后,在插入节点时,只需要在哨兵节点之后插入即可。这样就可以避免判断链表是否为空的情况。

假设需要查找链表中的某个值,那么可以尝试使用哨兵思想 以下我用 Python 实现的单链表的节点的值的查询

示例代码

class Node:
    def __init__(self, value, next=None):
        self.value = value
        self.next = next
class LinkedList:
    def __init__(self, values=None):
        self.dummy_head = Node(None)  # 哨兵节点
        self.size = 0
        if values:
            self.add_all(values)
    # 向链表中添加一个值序列
    def add_all(self, values):
        for value in values:
            self.add_last(value)
    # 向链表的最后添加一个节点
    def add_last(self, value):
        self.add_after(self.tail, value)
    # 在指定节点之后添加一个新节点
    def add_after(self, node, value):
        new_node = Node(value)
        new_node.next = node.next
        node.next = new_node
        self.size += 1
    @property
    # 返回链表的最后一个节点
    def tail(self):
        curr = self.dummy_head
        while curr.next:
            curr = curr.next
        return curr
    # 查找链表中是否存在一个值为给定值的节点
    def find(self, value):
        curr = self.dummy_head.next
        while curr and curr.value != value:
            curr = curr.next
        return curr
# 使用示例
linked_list = LinkedList([1, 2, 3, 4, 5])
node = linked_list.find(6)
if node:
    print(f'发现值: {node.value}')
else:
    print('未找到')
复制代码

复杂度分析

时间复杂度:O(n)

在最坏的情况下,我们需要遍历整个链表来找到目标值。因此,时间复杂度为O(n)。

空间复杂度:O(1)

我们只使用了常数个变量来存储信息,因此空间复杂度为O(1)。

解题思维导图

image.png

代码流程图:

image.png

其他

除了数组和链表,哨兵思想可以用于多种数据结构,例如栈、队列、哈希表、二叉树、图等。这里就不展示代码了,感兴趣的读者可以自行尝试编程。

在栈中,哨兵可以用于指示栈的底部。这样,当我们执行 push 操作时,就不需要先判断栈是否为空了。同样,当我们执行 pop 操作时,也不需要再判断栈是否为空了。

思路流程图

image.png

队列

在队列中,哨兵可以用于指示队列的首尾。这样,当我们执行入列操作时,就不需要先判断队列是否已满了。同样,当我们执行出列操作时,也不需要再判断队列是否为空了。

思路流程图

image.png

流程图解释:

  • 在队列中,哨兵节点用来指示队列的首尾。
  • 队头指针指向队列的第一个有效节点,队尾指针指向队列的最后一个有效节点。
  • 当我们执行出队操作时,会将队头指针向后移动一位;当我们执行入队操作时,会将队尾指针向后移动一位。

哈希表

在哈希表中,哨兵可以用于指示哈希表的桶。这样,当我们执行插入操作时,就不需要再判断桶是否已满了。同样,当我们执行查询操作时,也不需要再判断桶是否为空了。

可以参考如下流程我们需要按照如下流程:

  1. 创建哈希表。
  2. 创建一个数组来表示哈希表的桶,并在每个桶中插入一个哨兵节点。
  3. 对于每一个要插入哈希表的元素,先计算出它在哈希表中对应的桶的下标,然后将其插入到该桶的末尾。
  4. 对于每一个要查询的元素,先计算出它在哈希表中对应的桶的下标,然后在该桶中查找该元素。如果找到了,则返回查询结果;如果没有找到,则返回 null。
思路流程图

image.png

二叉树

在二叉树中,哨兵可以用于指示二叉树的空节点。这样,我们在遍历二叉树时,就不需要再判断节点是否为空了。

思路流程图

image.png

流程图解释:在上面的流程图中,我们将哨兵节点作为二叉树的根节点。在遍历二叉树时,我们可以直接跳过哨兵节点,而不需要再判断它是否为空。当我们遍历完一个节点的左右子节点后,可以直接回到哨兵节点,然后再继续遍历其他节点。

在图中,哨兵可以用于指示图的边界。这样,我们在遍历图时,就不需要再判断边界了。

思路流程图

image.png

流程图解释:在这个流程图中,我们将哨兵节点看作是图的边界。当我们执行插入操作时,会新建一个节点并将其插入到哨兵节点的后面;当我们执行删除操作时,会将哨兵节点的后继节点删除。

实战:回文链表

Tips此节在力扣做过该题目阅读效果更佳

题目地址:234. 回文链表 - 力扣(LeetCode)

题目:

网络异常,图片无法展示
|

思路:

  1. 将哨兵插入链表的开头,并使哨兵的 next 指针指向链表的第一个节点。
  2. 设置两个指针:快指针和慢指针。初始时,快指针指向哨兵,慢指针指向哨兵的 next 指针所指向的节点。
  3. 将快指针向前移动两个节点,将慢指针向前移动一个节点。重复这个过程,直到快指针指向链表的最后一个节点为止。
  4. 在移动快指针的过程中,将慢指针指向的节点的值存储在一个栈中。
  5. 当快指针指向链表的最后一个节点时,将慢指针重新定位到哨兵的 next 指针所指向的节点。
  6. 通过使用栈,依次比较慢指针指向的节点和栈顶元素是否相等。如果慢指针指向的节点和栈顶元素是相等的,则将栈顶元素弹出栈,并将慢指针向前移动一个节点。如果不相等,则说明链表不是回文的。重复这个过程,直到慢指针指向链表的最后一个节点为止。
  7. 如果在比较过程中,栈已经被弹空,说明链表是回文的。否则,链表不是回文的。

这种方法的时间复杂度为 O(n),空间复杂度为 O(n),其中 n 是链表中节点的数量。

示例代码

/**  
 * Definition for singly-linked list.  
 * struct ListNode {  
 *     int val;  
 *     struct ListNode *next;  
 * };  
 */  
bool isPalindrome(struct ListNode* head) {  
    // 如果链表为空或只有一个元素,直接返回 true  
    if (head == NULL || head->next == NULL) {  
        return true;  
    }  
    // 找到链表的中点  
    struct ListNode *slow = head;  
    struct ListNode *fast = head;  
    while (fast->next != NULL && fast->next->next != NULL) {  
        slow = slow->next;  
        fast = fast->next->next;  
    }  
    // 反转后半部分  
    struct ListNode *prev = NULL;  
    struct ListNode *curr = slow->next;  
    while (curr != NULL) {  
        struct ListNode *next = curr->next;  
        curr->next = prev;  
        prev = curr;  
        curr = next;  
    }  
    slow->next = prev;  
    // 比较前半部分和反转后的后半部分是否相等  
    struct ListNode *p1 = head;  
    struct ListNode *p2 = slow->next;  
    while (p2 != NULL) {  
        if (p1->val != p2->val) {  
            return false;  
        }  
        p1 = p1->next;  
        p2 = p2->next;  
    }  
    return true;  
}
复制代码

总结

本文从介绍哨兵思想的起源出发,讲了哨兵思想的定义和它在数组、链表等各个数据结构中使用的示例,最后给出一道leetcode的题目用于练习。

最后

本文所涉及的内容基于我的个人实践,很多东西基于我个人的理解,所以有望大家多多指教。

如果你发现了本文的错误之处,欢迎你在评论区留言,我会及时的进行修改。如果你有其他的想法,也欢迎在评论区留言,我会在看到评论的第一时间回复。

码字不易,如果你觉得本文对你有帮助,麻烦点个大大的赞,你的支持是我更新的最大动力!

目录
相关文章
|
6月前
|
存储 负载均衡 算法
基于 C++ 语言的迪杰斯特拉算法在局域网计算机管理中的应用剖析
在局域网计算机管理中,迪杰斯特拉算法用于优化网络路径、分配资源和定位故障节点,确保高效稳定的网络环境。该算法通过计算最短路径,提升数据传输速率与稳定性,实现负载均衡并快速排除故障。C++代码示例展示了其在网络模拟中的应用,为企业信息化建设提供有力支持。
163 15
|
7月前
|
运维 监控 算法
监控局域网其他电脑:Go 语言迪杰斯特拉算法的高效应用
在信息化时代,监控局域网成为网络管理与安全防护的关键需求。本文探讨了迪杰斯特拉(Dijkstra)算法在监控局域网中的应用,通过计算最短路径优化数据传输和故障检测。文中提供了使用Go语言实现的代码例程,展示了如何高效地进行网络监控,确保局域网的稳定运行和数据安全。迪杰斯特拉算法能减少传输延迟和带宽消耗,及时发现并处理网络故障,适用于复杂网络环境下的管理和维护。
|
2月前
|
存储 监控 安全
企业上网监控系统中红黑树数据结构的 Python 算法实现与应用研究
企业上网监控系统需高效处理海量数据,传统数据结构存在性能瓶颈。红黑树通过自平衡机制,确保查找、插入、删除操作的时间复杂度稳定在 O(log n),适用于网络记录存储、设备信息维护及安全事件排序等场景。本文分析红黑树的理论基础、应用场景及 Python 实现,并探讨其在企业监控系统中的实践价值,提升系统性能与稳定性。
59 1
|
6月前
|
分布式计算 并行计算 算法
MapReduce在实现PageRank算法中的应用
总结来说,在实现PageRank算法时使用MapReduce能够有效地进行大规模并行计算,并且具有良好的容错性和可扩展性。
210 76
|
4月前
|
监控 算法 JavaScript
公司局域网管理视域下 Node.js 图算法的深度应用研究:拓扑结构建模与流量优化策略探析
本文探讨了图论算法在公司局域网管理中的应用,针对设备互联复杂、流量调度低效及安全监控困难等问题,提出基于图论的解决方案。通过节点与边建模局域网拓扑结构,利用DFS/BFS实现设备快速发现,Dijkstra算法优化流量路径,社区检测算法识别安全风险。结合WorkWin软件实例,展示了算法在设备管理、流量调度与安全监控中的价值,为智能化局域网管理提供了理论与实践指导。
115 3
|
4月前
|
存储 监控 算法
基于 C# 时间轮算法的控制局域网上网时间与实践应用
在数字化办公与教育环境中,局域网作为内部网络通信的核心基础设施,其精细化管理水平直接影响网络资源的合理配置与使用效能。对局域网用户上网时间的有效管控,已成为企业、教育机构等组织的重要管理需求。这一需求不仅旨在提升员工工作效率、规范学生网络使用行为,更是优化网络带宽资源分配的关键举措。时间轮算法作为一种经典的定时任务管理机制,在局域网用户上网时间管控场景中展现出显著的技术优势。本文将系统阐述时间轮算法的核心原理,并基于 C# 编程语言提供具体实现方案,以期深入剖析该算法在局域网管理中的应用逻辑与实践价值。
94 5
|
4月前
|
存储 机器学习/深度学习 算法
论上网限制软件中 Python 动态衰减权重算法于行为管控领域的创新性应用
在网络安全与行为管理的学术语境中,上网限制软件面临着精准识别并管控用户不合规网络请求的复杂任务。传统的基于静态规则库或固定阈值的策略,在实践中暴露出较高的误判率与较差的动态适应性。本研究引入一种基于 “动态衰减权重算法” 的优化策略,融合时间序列分析与权重衰减机制,旨在显著提升上网限制软件的实时决策效能。
126 2
|
5月前
|
存储 监控 算法
公司员工电脑监控软件剖析:PHP 布隆过滤器算法的应用与效能探究
在数字化办公的浪潮下,公司员工电脑监控软件成为企业管理的重要工具,它能够帮助企业了解员工的工作状态、保障数据安全以及提升工作效率。然而,随着监控数据量的不断增长,如何高效地处理和查询这些数据成为了关键问题。布隆过滤器(Bloom Filter)作为一种高效的概率型数据结构,在公司员工电脑监控软件中展现出独特的优势,本文将深入探讨 PHP 语言实现的布隆过滤器算法在该软件中的应用。
95 1
|
6月前
|
存储 人工智能 算法
通过Milvus内置Sparse-BM25算法进行全文检索并将混合检索应用于RAG系统
阿里云向量检索服务Milvus 2.5版本在全文检索、关键词匹配以及混合检索(Hybrid Search)方面实现了显著的增强,在多模态检索、RAG等多场景中检索结果能够兼顾召回率与精确性。本文将详细介绍如何利用 Milvus 2.5 版本实现这些功能,并阐述其在RAG 应用的 Retrieve 阶段的最佳实践。
1345 1
通过Milvus内置Sparse-BM25算法进行全文检索并将混合检索应用于RAG系统

热门文章

最新文章