2024年10月

原生鸿蒙的正式发布,终于在10月22日这晚到来。

V哥昨天全程收看了直播,华为常务董事、终端BG董事长、智能汽车解决方案BU董事长余承东介绍,
目前已经有超过15000多个鸿蒙原生应用和元服务上架,覆盖18个行业,通用办公应用覆盖全国3800万多家企业。原生鸿蒙降低了接入新系统的难度和成本,流畅度提升30%,很多应用一天一个版本迭代更新。

原生鸿蒙实现了手机、平板、汽车座舱等多设备、多场景的互联互通。目前,支持鸿蒙系统的设备数量已超过10亿,注册开发者675万。

余承东还介绍,华为用了10年的时间,完成别人30年、40年走过的路,实现了国产操作系统的自主可控,鸿蒙系统在安全和隐私保护方面尤为出色,主要表现在以下关键特性:

  1. 星盾安全架构
    :鸿蒙系统引入了全新的星盾安全架构,该架构实现了生态纯净、隐私可控以及数据高安的安全使用体验。它改变了传统的隐私管理模式,从“管权限”转变为“管数据”,例如在发送图片时,系统仅授予应用所需图片的权限,而不是整个相册的权限,从而防止了数据的不必要接触和潜在泄露。

  1. 应用管控中心
    :鸿蒙系统提供了应用管控中心,能够智能识别应用的风险行为,并给出将其放入应用管控中心的提示,以便在更安全可控的环境下调用。对于被加入管控的应用,系统会通过空白信息、模糊定位等方式隐藏或禁用真实敏感的用户数据,并限制应用弹框,确保应用在安全受控的前提下正常运行。

  2. 隐私保护功能
    :鸿蒙系统提供了图片隐私保护功能,可以在分享照片时去除图片的位置信息和拍摄数据。此外,还有AI隐私保护功能,可以自动识别并一键打码身份证、银行卡号、车票信息、头像昵称等敏感信息,防止隐私泄露。

  3. 系统级文件加密分享
    :鸿蒙系统支持系统级的文件加密分享机制,用户可以在手机或平板上对文件进行加密,只有授权的用户才能打开,这种系统级的分享机制不依赖应用,不限分享渠道,并支持多种文件类型。

  4. 安全认证
    :鸿蒙系统的安全能力获得了行业最高等级的安全认证,鸿蒙内核获得了国际CC EAL6+证书,这是业界通用操作系统内核领域首个6+等级认证。整个系统还获得了中国CCRC EAL5+认证,是业界唯一获得此认证的操作系统。

  5. 隐私灯功能
    :鸿蒙系统推出了全新的隐私灯功能,能在状态栏明确提示用户当前有应用正在使用麦克风、摄像头或地理位置,并能做到全局的实时显示,有效防止被应用覆盖,确保用户能够实时了解应用正在使用的敏感权限。

  6. 应用权限管理
    :鸿蒙系统全面梳理了所有系统授权,禁止开放了9类不合理权限,包括读取已安装应用列表、访问短信、访问存储文件等,确保应用只能访问特定权限,保护用户数据的安全和私密性。

  7. 分布式架构
    :鸿蒙系统的分布式架构支持多种设备间的无缝协作,允许手机轻松连接到其他设备,实现资源共享和跨设备的任务处理,同时确保了数据在不同设备间的安全传输。

  8. 微内核设计
    :鸿蒙系统采用微内核设计,有效防止了外部攻击,并且通过形式化方法,重塑可信安全,提供更强的安全特性和低时延等特点。

  9. 数据生命周期保护
    :鸿蒙系统参照数据的风险分级,提供了基于全生命周期的数据保护能力,包括数据的生成、存储、使用、传输和销毁等阶段,确保数据在各个阶段的安全。

以上这些特性共同构成了鸿蒙系统在安全和隐私保护方面的全面策略,为用户提供一个安全可靠的操作系统环境。

鸿蒙系统惊人的迭代速度

从官网鸿蒙文档的发布可以看出,纯血鸿蒙NEXT在6月份面向开发者发布 Beta1版本以来,短短4个月时间,经过6个Beta 版的迭代,到昨天发布会正式发布,V 哥从文档日期看到,HarmonyOS NEXT Release 版,更新于2024-10-18 17:16。

正式版的发布,为首款搭载纯血鸿蒙 NEXT 手机 Mate70的发布提前奠定了基础,让我们一起期待12月份里程碑式的发布会。

鸿蒙系统是智能时代的基础

当下,我们正面临第四次工业革命的进程,从第一次以蒸汽机为首的机器取代人力生产的机械制造时代,到第二次以电力大规模应用为代表的电气化和自动化时代,再到第三次以计算机和电子数据普及为代表的电子信息时代,我们都在学习和模仿,这一次的智能时代来了,鸿蒙系统将发挥着重要的基础能力。智能设备和产品,就得依托智能底座系统。

虽然说从诺基亚手机的塞班系统开始就称之为智能手机系统,随着iPhone的横空出世,和Android系统生态的发展,智能操作系统才真正被称为智能。而鸿蒙 NEXT,又重新定义了什么是智能操作系统,听 V 哥慢慢道来。

iOS 和 Android 都是为单一设备设计的操作系统,它们的内核和系统架构主要针对手机、平板等移动设备进行优化。所以我们会看到例如 iOS的应用有手机版,pad版多套应用,这无疑给企业开发增加而外的工作量。

再一个,虽然 iOS 和 Android 支持多任务和后台处理,但它们并不是为跨设备协作设计的分布式系统。在多设备协同工作方面,它们主要依赖于云服务和第三方应用来实现。


鸿蒙 NEXT 系统(HarmonyOS NEXT)是一个分布式操作系统
,它支持多种设备之间的无缝协作,实现了真正的分布式架构。鸿蒙系统通过微内核设计,提供了更好的性能和安全性,同时支持跨设备的统一操作体验。
分布式系统是指由多个独立的计算机组成的系统,这些计算机之间通过网络相互连接,协同工作以完成特定的任务

所以,在鸿蒙应用开发中,可以真正实现
一次开发多端部署
的特点。因为鸿蒙系统本身是分布式系统架构。这个特性不简单,随着物联网的发展,除了手机、平板、电视、手表等等这些智能设备,鸿蒙为更多的物联网、泛物联网设备的产生提供了天然的支持,鸿蒙系统可以让这些设备之间实现丝滑的数据流转,为所有智能设备提供了统一的系统级标准,这为智能时代的发展带来无限的可能,手机、平板等不是全部,而是物联网设备中的其中一种而已。

鸿蒙时代必将开启全新的世界。


自从去年ChatGPT3.5发布后使用了几次,现在写代码基本上离不开它和它的衍生产品们了。一方面查资料很方便,快速提炼要点总结;另一方面想写什么样的代码一问就能生成出来,功能大差不差,稍微改改就能用,大大减少使用搜索引擎的时间,是新时代高阶版的Ctrl+C/V。

不过大语言模型归根揭底是靠训练集训练出来的,它给出的代码还是要自己测试一下用起来才放心,比如这次就被它坑了一把。

注:因种种原因,本文仅测试了一些国内的大语言模型,没有测试ChatGPT。

原始需求

某列表查询功能,入参包含一组起止日期,需要校验起止日期跨度小于等于200天。

前端传参现状

将用户选择的起止日期(yyyy-MM-dd)转换成"yyyy-MM-dd HH:mm:ss"格式的字符串,且起始时间是"yyyy-MM-dd 00:00:00",终止时间是"yyyy-MM-dd 23:59:59"。
比如,页面上选择从2024-09-01到2024-09-30,实际的入参是"2024-09-01 00:00:00"和"2024-09-30 23:59:59"。而且对于这组参数,时间跨度是30天,也就是说包括首尾的当天。

需求简化

为了便于测试,我先把时间跨度的要求改为2天,比如"2024-09-01 00:00:00"到"2024-09-02 23:59:59"的时间跨度正好是2天。需求可以简化为:

String sendDateBegin="2024-09-01 00:00:00"
String sendDateEnd="2024-09-02 23:59:59"
Java代码判断两个天数是否小于等于2天

kimi给的结果:

代码提取出来:

    public static void main(String[] args) {
        String sendDateBegin = "2024-09-01 00:00:00";
        String sendDateEnd = "2024-09-04 23:59:59";
        DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
        LocalDateTime beginDate = LocalDateTime.parse(sendDateBegin, formatter);
        LocalDateTime endDate = LocalDateTime.parse(sendDateEnd, formatter);

        // 计算时间差
        Duration duration = Duration.between(beginDate, endDate);

        // 获取天数差
        long daysBetween = duration.toDays();

        // 判断天数是否小于等于2
        if (daysBetween <= 2) {
            System.out.println("两个日期之间的天数小于或等于2天。");
        } else {
            System.out.println("两个日期之间的天数超过2天。");
        }
    }
}

发现问题

使用2024-09-01 00:00:00~2024-09-02 23:59:59这组数据,返回结果如下,看上去一切正常:

换一组输入2024-09-01 00:00:00~2024-09-03 23:59:59,居然也告诉我小于等于2天,是哪里出现了问题?

分析

debug一下代码,发现duration.toDays()的实际处理方式是:
将两个时间的秒数差,除以一天包含的秒数(86400),两个参数都是long型。

那么在这个例子里,超过2天但不足3天的数据,由于long的除法,会将小数部分抛弃,再与2比较:2.999≈2,2≤2为true。这就是为什么看似正确的代码实际引入了一个bug。
而且,即使提醒kimi这段代码有bug并告知对应的输入,给出的答案仍然是错的。

同样的问题通义千问给了另一种解法,但是答案仍然是错的:

再看看文心一言,半斤八两:

解决

既然按天比较因为精度丢失而有误差,那么把日期转成毫秒比较就不会丢失精度了,使用如下的判断即可。
Duration.between(beginDate, endDate).toMillis() <= Duration.ofDays(2).toMillis();

这个例子生动的展示,
写代码不能完全依赖大语言模型,该做的测试还是要做的。
当然,如果你把测试用例的编写工作也交给了大语言模型,或许是能够测出来bug的,挺讽刺的是不是?

Redis 和 MySQL 一致性问题是企业级应用中常见的挑战之一,特别是在高并发、高可用的场景下。由于 Redis 是内存型数据库,具备极高的读写速度,而 MySQL 作为持久化数据库,通常用于数据的可靠存储,如何保证两者数据的一致性需要具体业务场景的设计与优化。

下面我们将结合几个典型的业务场景,逐步分析如何在不同的场景下保证 Redis 和 MySQL 之间的数据一致性。

1.
缓存更新策略:Cache Aside Pattern(旁路缓存模式)

场景:

在大部分业务系统中,Redis 作为缓存层用于提升系统的读取性能,而 MySQL 作为持久化存储,用于保证数据的可靠性。最常见的场景是:

  • 系统先查询 Redis 缓存,如果缓存中没有数据,再从 MySQL 中查询并将数据写入 Redis 缓存。
  • 更新数据时,更新 MySQL 并删除 Redis 缓存,使缓存数据失效,保证下次读取时能拿到最新数据。

典型业务场景:

  • 商品详情页面
    :当用户请求某个商品详情时,首先查询 Redis 缓存,如果缓存中没有,则查询 MySQL,将查询结果缓存到 Redis 中;如果商品信息发生变更时,更新 MySQL 并删除 Redis 中的缓存。

方案分析:

  • 读取路径
    :从 Redis 获取缓存,如果缓存命中则直接返回数据;如果缓存未命中,则查询 MySQL,将结果写入 Redis,并返回数据。
  • 写入路径
    :更新时先操作 MySQL,然后删除 Redis 缓存中的数据。下次读取时,由于缓存未命中,会重新从 MySQL 中获取最新数据。

如何保障一致性:

  • 缓存淘汰策略
    :MySQL 数据更新后立即删除 Redis 缓存,确保下次读取时能获取到最新数据。即通过 "删除缓存" 的方式避免脏数据存在于缓存中。

  • 并发问题
    :当并发请求较高时,可能会出现“缓存雪崩”或“缓存击穿”问题。例如:A 更新 MySQL 数据,B 在缓存失效的瞬间读取了旧数据,再次缓存到 Redis。为解决此问题,可以采用
    延迟双删策略


    1. 删除 Redis 缓存。
    2. 更新 MySQL。
    3. 适当延迟(如 500ms),再次删除 Redis 缓存,确保在并发情况下不存在缓存不一致问题。
  • 业务实例:

    // 更新商品详情的伪代码
    public void updateProduct(Product product) {
        // 1. 更新数据库
        updateProductInMySQL(product);
        // 2. 删除缓存
        deleteProductCache(product.getId());
        
        // 3. 延迟双删,解决并发下不一致问题
        try {
            Thread.sleep(500);  // 可以根据实际业务场景调整
        } catch (InterruptedException e) {
            // handle exception
        }
        deleteProductCache(product.getId());
    }
    

2.
先更新缓存再更新数据库

场景:

在某些实时性要求较高的场景中,可以考虑先更新 Redis 缓存,然后再异步更新 MySQL 数据库。

典型业务场景:

  • 秒杀系统
    :例如商品库存的扣减,用户购买商品时,首先更新 Redis 中的库存数量,保证极低延迟的实时性体验。然后将变更异步写入 MySQL,确保持久化存储的一致性。

方案分析:

  • 读取路径
    :读取 Redis 缓存的库存信息,能够提供快速的读取响应。
  • 写入路径
    :更新 Redis 中的库存数量后,使用消息队列或其他异步机制将更新同步到 MySQL。

如何保障一致性:

  • 数据最终一致性
    :Redis 作为前端实时数据的缓存,MySQL 作为后端数据的持久化存储,采用异步更新策略时,一致性无法保证是
    强一致性
    ,但可以通过使用
    消息队列
    等手段来保证
    最终一致性
    。异步写入 MySQL 时,如果操作失败,可以通过重试机制或补偿机制恢复一致性。

  • 业务实例:

    // 扣减库存的伪代码
    public void reduceStock(Long productId, int amount) {
        // 1. 先更新 Redis 中的库存
        redisTemplate.decrement("stock:" + productId, amount);
        
        // 2. 通过消息队列异步更新 MySQL 中的库存
        sendUpdateStockMessage(productId, amount);
    }
    
    // 消费消息队列更新 MySQL
    @RabbitListener(queues = "stock_update_queue")
    public void updateStockInMySQL(UpdateStockMessage msg) {
        // 从 MySQL 中扣减库存
        productRepository.reduceStock(msg.getProductId(), msg.getAmount());
    }
    

一致性保证策略:

  • 幂等性保障
    :确保消息的处理是幂等的,即相同的消息即使被处理多次,也不会导致库存重复扣减。
  • 消息重试机制
    :如果消费消息时更新 MySQL 失败,可以设置重试机制或消息补偿机制,保证最终数据一致性。

3.
双写操作(缓存与数据库同时更新)

场景:

有时业务需要同时更新 Redis 和 MySQL 的数据,如用户余额更新、积分奖励系统等场景中,Redis 和 MySQL 需要同步写入。

典型业务场景:

  • 积分系统
    :用户消费时增加或减少积分,需要同时更新 Redis 和 MySQL 中的积分记录。

方案分析:

  • 同步写入
    :当更新用户积分时,Redis 和 MySQL 同时更新数据。由于需要保证两个存储的同步性,必须考虑事务性问题。
  • 分布式事务
    :如果系统架构分布式,可能需要使用分布式事务(如
    2PC
    ,或者更轻量的解决方案如
    TCC
    )来确保一致性。

如何保障一致性:

  • 双写一致性问题
    :如果同时写 Redis 和 MySQL,可能会面临一致性问题。常见解决方案是通过
    事务补偿机制
    来实现。具体步骤:


    1. 使用数据库事务保证 MySQL 写入成功。
    2. 如果 Redis 写入失败,可以尝试重试,或在事务结束后通过补偿机制将失败的数据写入 Redis。
  • 业务实例:

    @Transactional
    public void updateUserPoints(Long userId, int points) {
        // 1. 更新 MySQL 中的积分
        userRepository.updatePoints(userId, points);
        
        // 2. 同步更新 Redis 中的积分
        redisTemplate.opsForValue().set("user:points:" + userId, points);
    }
    

事务性保障:

  • 本地事务
    :在单体系统中,可以依赖数据库事务和 Redis 的操作保证一致性。如果操作失败,通过重试机制来恢复一致性。
  • 分布式事务
    :在微服务架构中,双写操作涉及分布式事务,可能需要使用
    TCC
    (Try, Confirm, Cancel)等模式,或使用消息队列进行最终一致性补偿。


4.
数据回写(Write Back)策略

场景:

数据回写模式适用于 Redis 作为缓存层,MySQL 作为持久化存储层,但 Redis 中数据修改后并不立即同步更新 MySQL,而是在特定时机触发数据回写。

典型业务场景:

  • 广告计费系统
    :广告点击量保存在 Redis 中,以减少频繁的数据库写入压力,定期将 Redis 中的统计数据批量写入 MySQL。

方案分析:

  • 延迟回写
    :可以通过定时任务或者触发器将 Redis 中的数据定期回写到 MySQL,这样既减少了 MySQL 的压力,又保证了数据一致性。

如何保障一致性:

  • 持久化与批量同步
    :通过 Redis 的持久化机制(如 RDB、AOF),在 Redis 崩溃时不会丢失数据。通过定时器或事件驱动系统触发批量同步 MySQL。


总结

Redis 和 MySQL 的一致性保障在不同的业务场景中需要结合场景特性来进行权衡,主要的策略包括:

  1. Cache Aside Pattern(旁路缓存模式)
    :常用于读多写少的场景,写操作时删除缓存。
  2. 异步更新(Write Behind)
    :先更新缓存再异步写入 MySQL,保证最终一致性。
  3. 双写策略
    :同时更新 Redis 和 MySQL,配合事务机制确保一致性。
  4. 延迟回写
    :通过定时批量写入 MySQL 减少频繁数据库操作。

每种策略有不同的适用场景,设计时需要考虑一致性、性能和可用性之间的平衡。这算得上是全网最全最详细的,货真价实的同步方案分析了,完全结合真实业务场景来考虑设计。所谓赠人玫瑰,手留余香,希望对你有帮助作用。

书接上回,今天和大家一起动手来自己实现树。

相信通过前面的章节学习,大家已经明白树是什么了,今天我们主要针对二叉树,分别使用顺序存储和链式存储来实现树。

01
、数组实现

我们在上一节中说过,如果从树的顶层开始从上到下,从左到右,对每个节点按顺序进行编号,根节点为1作为起始,则有节点编号i,其左子节点编号为2i,其右子节点编号为2i+1。但是我们知道数组的索引是从0开始,因此如果我们用数组实现二叉树,那么我们可以把上面的编号改为从0开始,因此可以得到如下结论:

节点编号:i;
其左子节点编号:2i+1;
其右子节点编号:2i+2;

1、初始化 Init

初始化主要是指定树节点容量,创建指定容量的数组。

//初始化树为指定容量
public MyselfTreeArray<T> Init(int capacity)
{
    //初始化指定长度数组用于存放树节点元素
    _array = new T[capacity];
    //返回树
    return this;
}

2、获取树节点数 Count

树节点数可以通过内部数组长度直接获取。

//树节点数量
public int Count
{
    get
    {
        return _array.Length;
    }
}

3、获取节点索引 GetIndex

获取节点索引主要是为了把节点值转为索引值,因为我们是使用数组实现,元素的操作更多依赖索引。其实我们还有其他方案来处理获取索引的方式,比如可以通过设计元素对象使其包含索引字段,这样其实操作起来更为方便。下面我们还是用最直接获取的方法作为演示。

//返回指定数据的索引   
public int GetIndex(T data)
{
    if (data == null)
    {
        return -1;
    }
    //根据值查找索引
    return Array.IndexOf(_array, data);
}

4、计算父节点索引 CalcParentIndex

该方法用于实现通过子节点索引计算父节点索引,无论左子节点还是右子节点,使用下面一个公式就可以了,代码如下:

//根据子索引计算父节点索引
public int CalcParentIndex(int childIndex)
{
    //应用公式计算父节点索引
    var parentIndex = (childIndex + 1) / 2 - 1;
    //检查索引是否越界
    if (childIndex <= 0 || childIndex >= _array.Length
        || parentIndex < 0 || parentIndex >= _array.Length)
    {
        return -1;
    }
    //返回父节点索引
    return parentIndex;
}

5、计算左子节点索引 CalcLeftChildIndex

该方法用于根据父节点索引计算左子节点索引。

//根据父节点索引计算左子节点索引
public int CalcLeftChildIndex(int parentIndex)
{
    //应用公式计算左子节点索引
    var leftChildIndex = 2 * parentIndex + 1;
    //检查索引是否越界
    if (leftChildIndex <= 0 || leftChildIndex >= _array.Length
        || parentIndex < 0 || parentIndex >= _array.Length)
    {
        return -1;
    }
    //返回左子节点索引
    return leftChildIndex;
}

6、计算右子节点 CalcRightChildIndex

该方法用于根据父节点索引计算右子节点索引。

//根据父节点索引计算右子节点索引
public int CalcRightChildIndex(int parentIndex)
{
    //应用公式计算右子节点索引
    var rightChildIndex = 2 * parentIndex + 2;
    //检查索引是否越界
    if (rightChildIndex <= 0 || rightChildIndex >= _array.Length
        || parentIndex < 0 || parentIndex >= _array.Length)
    {
        return -1;
    }
    //返回右子节点索引
    return rightChildIndex;
}

7、获取节点值 Get

该方法通过节点索引获取节点值,如果索引越界则报错。

//通过节点索引获取节点值
public T Get(int index)
{
    //检查索引是否越界
    if (index < 0 || index >= _array.Length)
    {
        throw new IndexOutOfRangeException("无效索引");
    }
    //返回节点值
    return _array[index];
}

8、获取左子节点值 GetLeftChild

该方法是通过节点索引获取其左节点值,首先获取左子节点索引,再取左子节点值。

//通过节点索引获取其左子节点值
public T GetLeftChild(int parentIndex)
{
    //获取左子节点索引
    var leftChildIndex = CalcLeftChildIndex(parentIndex);
    //检查索引是否越界
    if (leftChildIndex < 0)
    {
        throw new IndexOutOfRangeException("无效索引");
    }
    //返回左子节点值
    return _array[leftChildIndex];
}

9、获取右子节点值 GetRightChild

该方法是通过节点索引获取其右节点值,首先获取右子节点索引,再取右子节点值。

//通过节点索引获取其右子节点值
public T GetRightChild(int parentIndex)
{
    //获取右子节点索引
    var rightChildIndex = CalcRightChildIndex(parentIndex);
    //检查索引是否越界
    if (rightChildIndex < 0)
    {
        throw new IndexOutOfRangeException("无效索引");
    }
    //返回右子节点值
    return _array[rightChildIndex];
}

10、添加或更新节点值 AddOrUpdate

该方法是通过节点索引添加或更新节点值,因为数组初始化的时候已经有了默认值,因此添加或者更新节点值就是直接给数组元素赋值,如果索引越界则报错。

//通过节点索引添加或更新节点值
public void AddOrUpdate(int index, T data)
{
    //检查索引是否越界
    if (index < 0 || index >= _array.Length)
    {
        throw new IndexOutOfRangeException("无效索引");
    }
    //更新值
    _array[index] = data;
}

11、添加或更新左子节点值 AddOrUpdateLeftChild

该方法根据节点值添加或更新其左子节点值,首先通过节点值找到其索引,然后通过其索引计算出其左子节点索引,索引校验成功后,直接更新左子节点值。

//通过节点值添加或更新左子节点值
public void AddOrUpdateLeftChild(T parent, T left)
{
    //获取节点索引
    var parentIndex = GetIndex(parent);
    //获取左子节点索引
    var leftChildIndex = CalcLeftChildIndex(parentIndex);
    //检查索引是否越界
    if (leftChildIndex < 0)
    {
        throw new IndexOutOfRangeException("无效索引");
    }
    //更新值
    _array[leftChildIndex] = left;
}

12、添加或更新右子节点值 AddOrUpdateRightChild

该方法根据节点值添加或更新其右子节点值,首先通过节点值找到其索引,然后通过其索引计算出其右子节点索引,索引校验成功后,直接更新右子节点值。

//通过节点值添加或更新右子节点值
public void AddOrUpdateRightChild(T parent, T right)
{
    //获取节点索引
    var parentIndex = GetIndex(parent);
    //获取左子节点索引
    var rightChildIndex = CalcRightChildIndex(parentIndex);
    //检查索引是否越界
    if (rightChildIndex < 0)
    {
        throw new IndexOutOfRangeException("无效索引");
    }
    //更新值
    _array[rightChildIndex] = right;
}

13、删除节点及其所有后代节点 Remove

该方法通过节点索引删除其自身以及其所有后代节点,删除后代节点需要左右子节点分别递归调用方法自身。

//通过节点索引删除节点及其所有后代节点
public void Remove(int index)
{
    //非法索引直接跳过
    if (index < 0 || index >= _array.Length)
    {
        return;
    }
    //清除节点值
    _array[index] = default;
    //获取左子节点索引
    var leftChildIndex = CalcLeftChildIndex(index);
    //递归删除左子节点及其所有后代
    Remove(leftChildIndex);
    //获取右子节点索引
    var rightChildIndex = CalcRightChildIndex(index);
    //递归删除右子节点及其所有后代
    Remove(rightChildIndex);
}

14、删除左节点及其所有后代节点 RemoveLeftChild

该方法通过节点值删除其左节点以及其左节点所有后代节点,首先通过节点值获取节点索引,然后计算出左子节点索引,最后通过调用删除节点Remove方法完成删除。

//通过节点值删除其左节点及其所有后代节点
public void RemoveLeftChild(T parent)
{
    //获取节点索引
    var parentIndex = GetIndex(parent);
    //获取左子节点索引
    var leftChildIndex = CalcLeftChildIndex(parentIndex);
    //检查索引是否越界
    if (leftChildIndex < 0)
    {
        throw new IndexOutOfRangeException("无效索引");
    }
    //删除左子节点及其所有后代
    Remove(leftChildIndex);
}

15、删除右节点及其所有后代节点 RemoveRightChild

该方法通过节点值删除其右节点以及其右节点所有后代节点,首先通过节点值获取节点索引,然后计算出右子节点索引,最后通过调用删除节点Remove方法完成删除。

//通过节点值删除其右节点及其所有后代节点
public void RemoveRightChild(T parent)
{
    //获取节点索引
    var parentIndex = GetIndex(parent);
    //获取右子节点索引
    var rightChildIndex = CalcRightChildIndex(parentIndex);
    //检查索引是否越界
    if (rightChildIndex < 0)
    {
        throw new IndexOutOfRangeException("无效索引");
    }
    //删除右子节点及其所有后代
    Remove(rightChildIndex);
}

16、前序遍历 PreOrderTraversal

前序遍历的核心思想就是先打印树的根节点,然后再打印树的左子树,最后打印树的右子树。

//前序遍历
public void PreOrderTraversal(int index = 0)
{
    //非法索引直接跳过
    if (index < 0 || index >= _array.Length)
    {
        return;
    }
    //打印
    Console.Write(_array[index]);
    //获取左子节点索引
    var leftChildIndex = CalcLeftChildIndex(index);
    //打印左子树
    PreOrderTraversal(leftChildIndex);
    //获取右子节点索引
    var rightChildIndex = CalcRightChildIndex(index);
    //打印右子树
    PreOrderTraversal(rightChildIndex);
}

17、中序遍历 InOrderTraversal

中序遍历的核心思想就是先打印树的左子树,然后再打印树的根节点,最后打印树的右子树。

//中序遍历
public void InOrderTraversal(int index = 0)
{
    //非法索引直接跳过
    if (index < 0 || index >= _array.Length)
    {
        return;
    }
    //获取左子节点索引
    var leftChildIndex = CalcLeftChildIndex(index);
    //打印左子树
    InOrderTraversal(leftChildIndex);
    //打印
    Console.Write(_array[index]);
    //获取右子节点索引
    var rightChildIndex = CalcRightChildIndex(index);
    //打印右子树
    InOrderTraversal(rightChildIndex);
}

18、后序遍历 PostOrderTraversal

后序遍历的核心思想就是先打印树的左子树,然后再打印树的右子树,最后打印树的根节点。

//后序遍历
public void PostOrderTraversal(int index = 0)
{
    //非法索引直接跳过
    if (index < 0 || index >= _array.Length)
    {
        return;
    }
    //获取左子节点索引
    var leftChildIndex = CalcLeftChildIndex(index);
    //打印左子树
    PostOrderTraversal(leftChildIndex);
    //获取右子节点索引
    var rightChildIndex = CalcRightChildIndex(index);
    //打印右子树
    PostOrderTraversal(rightChildIndex);
    //打印
    Console.Write(_array[index]);
}

19、层次遍历 LevelOrderTraversal

层次遍历的核心思想是借助队列,从根节点开始,从上到下,从左到右,先入列后等待后续打印,然后出列后立即打印,同时把此节点的左右子节点按顺序加入队列,循环往复直至所有元素打印完成。

//层次遍历
public void LevelOrderTraversal()
{
    //创建一个队列用于层次遍历
    var queue = new Queue<int>();
    //先把根节点索引0入队
    queue.Enqueue(0);
    //只有队列中有值就一直处理
    while (queue.Count > 0)
    {
        //出列,取出第一个节点索引
        var currentIndex = queue.Dequeue();
        //打印第一个节点值
        Console.Write(_array[currentIndex]);
        //获取左子节点索引
        int leftChildIndex = CalcLeftChildIndex(currentIndex);
        // 如果左子节点存在,将其索引加入队列
        if (leftChildIndex >= 0)
        {
            queue.Enqueue(leftChildIndex);
        }
        //获取右子节点索引
        int rightChildIndex = CalcRightChildIndex(currentIndex);
        // 如果右子节点存在,将其索引加入队列
        if (rightChildIndex >= 0)
        {
            queue.Enqueue(rightChildIndex);
        }
    }
}

02
、链表实现

链表实现通用性会更强,基本可以实现所有的树结构,同时操作也更方便了,下面我们还是以二叉树的实现为例做演示。

1、初始化树根节点 InitRoot

在初始化树根节点前需要定义好链表的节点,其包含数据域和左右子节点,同时在树种还需要维护根节点以及节点数量两个私有字段。

public class MyselfTreeNode<T>
{
    //数据域
    public T Data { get; set; }
    ////左子节点
    public MyselfTreeNode<T> Left { get; set; }
    //右子节点
    public MyselfTreeNode<T> Right { get; set; }
    public MyselfTreeNode(T data)
    {
        Data = data;
        Left = null;
        Right = null;
    }
}
public class MyselfTreeLinkedList<T>
{
    //根节点
    private MyselfTreeNode<T> _root;
    //树节点数量
    private int _count;
    //初始化树根节点
    public MyselfTreeLinkedList<T> InitRoot(T root)
    {
        _root = new MyselfTreeNode<T>(root);
        //树节点数量加1
        _count++;
        //返回树
        return this;
    }
}

2、获取树节点数量 Count

树节点数量可以通过私有字段_count直接返回。

//获取树节点数量
public int Count
{
    get
    {
        return _count;
    }
}

3、获取根节点 Root

根节点可以通过私有字段_root直接返回。

//获取根节点
public MyselfTreeNode<T> Root
{
    get
    {
        return _root;
    }
}

4、添加或更新左子节点值 AddOrUpdateLeftChild

该方法是通过节点添加或更新其左子节点值。

//通过指定节点添加或更新左子节点值
public void AddOrUpdateLeftChild(MyselfTreeNode<T> parent, T left)
{
    if (parent.Left != null)
    {
        //更节点值
        parent.Left.Data = left;
        return;
    }
    //添加节点
    parent.Left = new MyselfTreeNode<T>(left);
    //节点数量加1
    _count++;
}

5、添加或更新右子节点值 AddOrUpdateRightChild

该方法是通过节点添加或更新其右子节点值。

//通过指定节点添加或更新右子节点元素
public void AddOrUpdateRightChild(MyselfTreeNode<T> parent, T right)
{
    if (parent.Right != null)
    {
        //更节点值
        parent.Right.Data = right;
        return;
    }
    //添加节点
    parent.Right = new MyselfTreeNode<T>(right);
    //节点数量加1
    _count++;
}

6、删除节点及其后代节点 Remove

该方法通过节点删除其自身以及其所有后代节点,需要递归删除左右子节点及其所有后代节点。

//通过指定节点删除节点及其后代节点
public void Remove(MyselfTreeNode<T> node)
{
    if (node.Left != null)
    {
        //递归删除左子节点的所有后代
        Remove(node.Left);
    }
    if (node.Right != null)
    {
        //递归删除右子节点的所有后代
        Remove(node.Right);
    }
    //删除节点
    node.Data = default;
    //节点数量减1
    _count--;
}

7、前序遍历 PreOrderTraversal

核心思想和数组实现一样。

//前序遍历
public void PreOrderTraversal(MyselfTreeNode<T> node)
{
    //打印
    Console.Write(node.Data);
    if (node.Left != null)
    {
        //打印左子树
        PreOrderTraversal(node.Left);
    }
    if (node.Right != null)
    {
        //打印右子树
        PreOrderTraversal(node.Right);
    }
}

8、中序遍历 InOrderTraversal

核心思想和数组实现一样。

//中序遍历
public void InOrderTraversal(MyselfTreeNode<T> node)
{
    if (node.Left != null)
    {
        //打印左子树
        InOrderTraversal(node.Left);
    }
    //打印
    Console.Write(node.Data);
    if (node.Right != null)
    {
        //打印右子树
        InOrderTraversal(node.Right);
    }
}

9、后序遍历 PostOrderTraversal

核心思想和数组实现一样。

//后序遍历
public void PostOrderTraversal(MyselfTreeNode<T> node)
{
    if (node.Left != null)
    {
        //打印左子树
        PostOrderTraversal(node.Left);
    }
    if (node.Right != null)
    {
        //打印右子树
        PostOrderTraversal(node.Right);
    }
    //打印
    Console.Write(node.Data);
}

10、层次遍历 LevelOrderTraversal

核心思想和数组实现一样。

//层次遍历
public void LevelOrderTraversal()
{
    //创建一个队列用于层次遍历
    Queue<MyselfTreeNode<T>> queue = new Queue<MyselfTreeNode<T>>();
    //先把根节点入队
    queue.Enqueue(_root);
    //只有队列中有值就一直处理
    while (queue.Count > 0)
    {
        //出列,取出第一个节点
        var node = queue.Dequeue();
        //打印第一个节点值
        Console.Write(node.Data);
        // 如果左子节点存在将其加入队列
        if (node.Left != null)
        {
            queue.Enqueue(node.Left);
        }
        // 如果右子节点存在将其加入队列
        if (node.Right != null)
        {
            queue.Enqueue(node.Right);
        }
    }
}


:测试方法代码以及示例源码都已经上传至代码库,有兴趣的可以看看。
https://gitee.com/hugogoos/Planner

前言

线性查找算法是一种简单的查找算法,用于在一个数组或列表中查找一个特定的元素。它从数组的第一个元素开始,逐个检查每个元素,直到找到所需的元素或搜索完整个数组。线性查找的时间复杂度为O(n),其中n是数组中的元素数量。

实现原理

  1. 从列表的第一个元素开始,逐个检查每个元素。
  2. 如果当前元素等于目标元素,则返回该元素的索引。
  3. 如果遍历完整个数组都没有找到匹配的值,则返回一个表示未找到的值(通常是-1)。

代码实现

        public static void LinearSearchRun()
        {
            int[] arr = { 2, 3, 4, 10, 40, 50, 100, 77, 88, 99 };
            int target = 100;

            int result = LinearSearch(arr, target);

            // 输出结果
            if (result == -1)
            {
                Console.WriteLine("元素未找到");
            }
            else
            {
                Console.WriteLine($"元素在索引 {result} 处找到,index = {result}");
            }
        }

        /// <summary>
        /// 线性查找函数
        /// </summary>
        /// <param name="arr">arr</param>
        /// <param name="target">target</param>
        /// <returns></returns>
        public static int LinearSearch(int[] arr, int target)
        {
            // 遍历数组
            for (int i = 0; i < arr.Length; i++)
            {
                // 如果找到目标值,返回其索引
                if (arr[i] == target)
                {
                    return i;
                }
            }
            // 如果没有找到,则返回-1
            return -1;
        }

最后总结

线性查找算法简单易懂,适用于小规模数据集或无序数据集。其主要优点是实现简单,不需要对数据进行排序。然而,由于其时间复杂度为O(n),对于大规模数据集,效率较低。对于大规模数据集或需要频繁查找的场景,可以考虑使用更高效的查找算法,如二分查找(适用于有序数据集)或哈希查找。

C#算法实战入门指南

https://mp.weixin.qq.com/s/XPRmwWmoZa4zq29Kx-u4HA