通用树查找算法

news/2024/7/7 19:04:06

        想要一个树形控件来显示数据,却发现Racket的GUI库竟然没有提供这个控件。既然没有,那就自己手搓一个吧。没想到,在做这个控件中竟然有了新发现!

        树形控件有一个功能是查找树中指定的节点。这就是接下来的故事的起点。

1 找外援

        不想动脑筋,就找网络,网上一般都是二叉树、红黑树啥的,不是我需要的;找AI帮我写一个,如下:

问:用Racket写一个通用的树查找算法。

AI答:

以下是使用Racket编写的通用树查找算法:

#lang racket
(define (tree? x)
  (and (pair? x) (not (null? x))))
(define (search-tree t target)
  (cond ((null? t) #f)
        ((equal? (car t) target) t)
        (else (+ 1 (search-tree (cdr t) target)))))
(define tree
  '(("A" "B" "C") ("D" "E" "F") ("G" "H" "I")))
(displayln "Tree:")
(tree-walk tree tree-root tree)
(displayln "Search for 'D':")
(search-tree tree 'D)

        这个程序定义了一个辅助函数 ‘tree?‘,用于检查一个值是否为树。然后,它定义了 ‘search-tree‘ 函数,该函数接受一个树和目标值作为参数,并返回找到的目标节点或 "#f"(表示未找到)。最后,它创建了一个名为“tree”的示例树,并使用“tree-walk”函数遍历它。然后,它在树中搜索目标值“D”,并将结果打印到控制台上。

显然,AI也不是救世主!还是开动自己的思想解决问题吧。

2 我的思考

         树的根节点、子节点、叶子节点的处理属于基本操作,不用过多考虑。重点是不确定的子节点数量,如何处理?

        以下是一个通用树节点的结构:

;树节点结构:
(struct node
(id ;这里设定id值为数字。
 value ;节点值。
 children ;子节点,考虑用列表来存放。
 ))

        我实际要使用的树节点比这个要复杂一些,这里我就把这个结构来凑合作为例子样本吧。

        如上面所说,重点就是children这个列表了。一般会考虑采用"for-each"遍历列表元素,然后用递归方式对每个元素的children继续以上过程,直到找到需要的值或遍历整个树为止。

        但这样有一个严重的问题,这样的每次递归都会出现等待下一级递归结束后才能继续列表元素处理的问题,就会出现著名的低效递归的问题——因为这样递归在执行时需要系统提供隐式栈来实现递归,造成效率低效,甚至填满内存的严重问题(当然现在内存一般足够大,还不至于因内存溢出造成崩溃)。

        解决办法是采用尾递归。难点还是在children列表上。

        我找到一个办法——将列表与树结合起来进行尾递归,算法原理如下。

3 算法原理

        假设我们有一个查找函数"find-node",其参数有两个——"tree"为要查找的树;"id"为查找的关键值。

        在"find-node"中定义一个闭包"find-node/in",它来实现树的遍历查找,这也是常规操作。

        为了实现尾递归,为"find-node/in"设定一个参数nodes作为存放树节点的子节点的列表(根节点是这个列表参数的初始值)。然后挨个搜索树的各个节点,如果遇到节点有子节点时,就把这个节点的children添加到nodes的后面。这样就把对树的搜索和对子节点列表的遍历转换为对列表nodes的遍历,处理起来就异常简单了。关键是,复杂度也很理想,这个最后分析。

具体算    法描述如下:

  1. 首先,nodes为包含节点的列表。

  2. 初始时nodes里仅包含根节点。

  3. 如果nodes列表为空,返回#f;

  4. 如果nodes列表不为空,取列表首值(节点):

  5. 如果该值是指定节点,返回该节点;

  6. 如果该值不是指定节点,检查其子节点:

  7. 如果有子节点,将子节点加入到nodes列表里,并重复3-6对nodes向后搜索;

  8. 如果没有子节点,重复3-6对nodes向后搜索。

        注意,以上算法描述中还需要考虑一个特例,就是根节点不存在(一般设置为"#f")时的情况。为了处理这个特例,我们在取出列表首值后首先对其做一个判断——是否为未设置节点值的根节点。

4 实现树查找

        根据上边的算法原理,我们把代码写出来,如下:

;定义取得节点值函数:
;tree为需要搜索的树,id为节点标识。
(define (find-node-version-1 tree id)
  (define (find-node/in nodes)
    (cond
      ;nodes列表为空,返回#f:
      [(empty? nodes) #f]
      ;值(节点)#f,即树根未初始化,树不存在:
      [(not (car nodes)) #f]
      ;nodes列表首值(节点)即为指定节点,返回节点:
      [(= (node-id (car nodes)) id)
       ;(display-node-value (car nodes))
       (car nodes)]
      ;如果nodes首值不是指定节点,且其子列表为空:
      [(empty? (node-children (car nodes)))
       ;(display-node-value (car nodes))
       (find-node/in (cdr nodes))]
      [else
       ;(display-node-value (car nodes))
       (find-node/in
        (append (cdr nodes)
                (node-children (car nodes))))]))
  (find-node/in (list tree)))

5 测试

        现在手搓一棵树并放到测试模块中来做测试。如下:

(module+ test
  ;测试空树:
  (define tree/empty #f)
  (find-node tree/empty 3)
  ;测试自定义树:
  (define tree/user
    (node 0 "t-0" (list (node 1 "t-1" '())
                        (node 2 "t-2" (list (node 3 "t-3" (list (node 4 "t-4" '())))
                                            (node 5 "t-5" '())
                                            (node 6 "t-6" (list (node 7 "t-7" '())
                                                                (node 8 "t-8" '())))))
                        (node 9 "t-9" (list (node 10 "t-10" (list (node 11 "t-11" '())))
                                            (node 12 "t-12" '())))
                        (node 13 "t-13" (list (node 14 "t-14" (list (node 15 "t-15" (list (node 16 "t-16" '()))))))))))
  (let* ([id 13]
         [n (find-node-version-1 tree/user id)])
    (display (format "查找结果为:id=~a,value=”~a“\n" id (node-value n))))
  )

这棵树如果在手机这种移动设备上看可能有点乱。如果用图片显示应该是这样的:

        运行程序,得到的结果是:

        查找结果为:id=13,value=”t-13“

6 检查搜索路径

        我们来看看程序的搜索路径。

        为了实现这个功能,我再定义一个显示函数,以便显示出程序搜索的每一个节点情况。如下:

;显示节点值(用于测试):
(define (display-node-value node)
  (display
   (format "id=~a,value=”~a“\n"
           (node-id node)
           (node-value node))))

        也就是上边程序中被注释掉的函数。现在我们把它的注释去掉。再运行程序,得到以下结果:

        id=0,value=”t-0“

        id=1,value=”t-1“

        id=2,value=”t-2“

        id=9,value=”t-9“

        id=13,value=”t-13“

        哦,这不是遍历树的广度优先算法吗!这的确是遍历树的广度优先算法。

        我们可以把这个算法独立出来用单独函数表示。如下:

;广度优先搜索:
(define (find-node/width nodes id)
  (cond
    ;nodes列表为空,返回#f:
    [(empty? nodes) #f]
    ;值(节点)#f,即树根未初始化,树不存在:
    [(not (car nodes)) #f]
    ;nodes列表首值(节点)即为指定节点,返回节点:
    [(= (node-id (car nodes)) id)
     ;(display-node-value (car nodes))
     (car nodes)]
    ;如果nodes首值不是指定节点,且其子列表为空:
    [(empty? (node-children (car nodes)))
     ;(display-node-value (car nodes))
     (find-node/width (cdr nodes) id)]
    [else
     ;(display-node-value (car nodes))
     (find-node/width
      (append (cdr nodes)
              (node-children (car nodes)))
      id)]))

相应地,查找函数就表示为:

;定义取得节点值函数:
;tree为需要搜索的树,id为节点标识。
(define (find-node tree id)
  ;广度优先搜索指定节点:
  (display (format "广度优先搜索指定节点:\n"))
  (find-node/width (list tree) id))

        在测试模块中添加如下测试代码:

(let* ([id 13]
       [n (find-node tree/user id)])
    (display (format "查找结果为:id=~a,value=”~a“\n" id (node-value n))))

        运行程序,结果如下:

        广度优先搜索指定节点:

        id=0,value=”t-0“

        id=1,value=”t-1“

        id=2,value=”t-2“

        id=9,value=”t-9“

        id=13,value=”t-13“

        与上边一样的结果。

        那么问题来了,如果要实现深度优先算法,怎么做呢?

7 深度优先算法

        从上边的算法原理来理解,如果在将子节点与现有nodes列表合并时,不是将子节点放到nodes列表之后,而是放到nodes列表之前——也就是说优先搜索子节点内容——就可以实现深度优先算法了。实现如下:

;深度优先搜索:
(define (find-node/depth nodes id)
  (cond
    ;nodes列表为空,返回#f:
    [(empty? nodes) #f]
    ;值(节点)#f,即树根未初始化,树不存在:
    [(not (car nodes)) #f]
    ;nodes列表首值(节点)即为指定节点,返回节点:
    [(= (node-id (car nodes)) id)
     ;(display-node-value (car nodes))
     (car nodes)]
    ;如果nodes首值不是指定节点,且其子列表为空:
    [(empty? (node-children (car nodes)))
     ;(display-node-value (car nodes))
     (find-node/depth (cdr nodes) id)]
    [else
     ;(display-node-value (car nodes))
     (find-node/depth
      (append (node-children (car nodes))
              (cdr nodes))
      id)]))

        把深度优先算法添加到"find-node"函数中,如下:

;定义取得节点值函数:
;tree为需要搜索的树,id为节点标识。
(define (find-node tree id)
  ;广度优先搜索指定节点:
  (display (format "广度优先搜索指定节点:\n"))
  (find-node/width (list tree) id)
  ;深度优先搜索指定节点:
  (display (format "深度优先搜索指定节点:\n"))
  (find-node/depth (list tree) id))

        运行程序,得到如下结果:

广度优先搜索指定节点:

id=0,value=”t-0“

id=1,value=”t-1“

id=2,value=”t-2“

id=9,value=”t-9“

id=13,value=”t-13“

深度优先搜索指定节点:

id=0,value=”t-0“

id=1,value=”t-1“

id=2,value=”t-2“

id=3,value=”t-3“

id=4,value=”t-4“

id=5,value=”t-5“

id=6,value=”t-6“

id=7,value=”t-7“

id=8,value=”t-8“

id=9,value=”t-9“

id=10,value=”t-10“

id=11,value=”t-11“

id=12,value=”t-12“

id=13,value=”t-13

        从上边的运行结果来看,广度优先算法比深度优先算法更快找到了指定的节点。当然,仅是对于这里给的数据样本及指定的id来说的。比如,如果id=8,运行结果就会出现深度优先算法更快找到结果的情况。

8 时间复杂度与空间复杂度

        从以上分析和运行情况可以直观看出,本查找算法的时间复杂度与空间复杂度均为O(n),而且是稳定算法。

           注:以上内容采用Racket语言的Scribble编辑并编译生成。


http://lihuaxi.xjx100.cn/news/2195909.html

相关文章

Game Fi链游:定制开发源码搭建

随着科技的飞速发展,游戏开发行业正经历着前所未有的变革。Game Fi,这个新兴的概念,正以其独特的魅力,引发了游戏开发行业的热潮。 一、Game Fi 的概念与影响 Game Fi,是一种新型的游戏开发模式,它强调游…

Docker 容器 mysql 配置主从

1、前提条件 集群的条件下 服务器 172.16.11.195 13316:3306 服务器 172.16.11.196 13317:3306 配置好主数据库和从数据 2、配置主从数据库 2.1使用portainer 来管理容器 建立数据库密码 新增配置文件 # mysql-master.cnf [mysqld] server_id110 log-binmysql-binrela…

漏洞挖掘 | 记一次信息泄露到登入后台

这次是项目上遇到的一个洞,打开页面是一个红红的登录页面 这里就不放图了,浓浓的红色气息~ 老样子抓登录包 虽然是明文传输但是爆破弱口令无果 f12大法,审计源代码,在其中一个js文件中发现了这个接口 拼接URL进行访问 感觉有点东…

信创国产化 | 聚铭网络携手银河麒麟完成产品兼容性互认证

在我国信创国产化战略深入推进的大背景下,聚铭网络与麒麟软件积极响应国家号召,共同致力于软件和操作系统的国产化发展。近日,双方宣布已完成产品兼容性互认证工作,这一成果标志着两家公司在信创国产化道路上迈出了坚实的一步。 …

TypeScript (TS) 类型定义总结

TypeScript (TS) 类型定义允许开发者为变量、函数参数、返回值等添加静态类型,这有助于提升代码的健壮性和可维护性。以下是一些使用TS类型定义的具体技巧及其例子: 1. 基础类型定义 基本类型如number, string, boolean等可以直接用于变量声明。 let …

STM32智能小车学习笔记(避障、循迹、跟随)

我们使用的是STM32CubeMX软件和MDK5 芯片使用的是STM32F103C8T6 完成对STM32CubeMX的初始化后开始我们的第一步点亮一个LED灯 一、点亮LED灯 点亮PC13连接的灯 打开STM32CubeMX软件,pc13设置为输出模式 然后按照这样配置,user label 设置成这个IO口代…

鸿蒙emitter 订阅事件封装 EmitterUtils

适用于api11 和api12 废话不多说,直接上代码 import emitter from ohos.events.emitter; import { StringUtils } from ohos/flutter_ohos;export class EmitterUtils{/*** 发射字符串类型的* param eventId* param data*/public static sendEvent(eventId:stri…

OceanBase v4.2 特性解析:Lateral Derived Table 优化查询

前言 从传统规则来看,内联视图通常不允许引用在同一FROM子句中前面定义的表的列。但从OceanBase 4.2.2版本开始,这一限制得到了突破,允许内联视图作为Lateral Derived Table来定义,从而允许此类引用。Lateral Derived Table的语法…