一同事公务员上岸了,千辛万苦,终于实现了工资从25000降到5000的飞跃。。。...

eadf9e751aeb820c2169ddaba90a0d87.gif

(关注数据结构和算法,了解更多新知识)

最近一网友发文称自己的同事公务员上岸了,实现了工资从25000降到5000。说实话这个工资降幅还是挺大的,如果是35岁之前成功上岸还是很值得的。

47f48dc5c6a0df613c739f3b6fb931fc.png

公务员职位虽然稳定,但薪资水平却往往无法与一些高薪的民营企业相提并论。然而,对于这位同事来说,这种转变或许并非完全出于薪资的考虑。毕竟,公务员的职业具有其独特的吸引力,如稳定的职业前景、良好的福利待遇以及相对轻松的工作环境等。或许,对于他来说,这些优势足以弥补薪资上的差距。

我们来看下各位网友的评论,大家更倾向于考公务员,有的网友说了:公务员不能看月薪,要看年终奖。

6643ba34703ea0b25515e054f724e816.png

4a2415f9f8b94e69936c14d738aae0bf.png

179521f894298c33c31b37447b5da276.png

ce41f012bb035b61e4ea639963f044c4.png

5577e28e3ae72e9c38714c42cd360b4b.png

079cf9f395c38f8f26433b122d80a7b6.png

--------------下面是今天的算法题--------------

今天就不讲LeetCode上的题了,我们来讲一个在LeetCode中经常用到的数据结构:线段树。

假设需要频繁求数组的区间和,我们可能会想到树状数组,或者是前缀和,但如果是求区间的最大值或者区间最小值呢?很明显使用树状数组或者前缀和是无能为力了,但我们可以使用另外一种数据结构-线段树。

线段树是一棵平衡的二叉搜索树,它将每个长度大于 1 的区间划分为左右两个区间递归求解。如果整个区间的长度为 n ,则线段树有 n 个叶子节点,每个叶子节点代表一个单位区间,每个内部节点可以直接获取区间的值(最大值,最小值,区间和等)。

线段树是建立在线段的基础上,每个节点都代表了一条线段 [a,b] ,在叶子节点中 a==b ,对于非叶子节点它的左子节点区间为 [a,(a+b)/2] ,右子节点区间为 [(a+b)/2+1,b] 。我们知道树状数组可以进行区间的修改及查询,但树状数组主要用于区间的求和,功能比较单一,而线段树不光能用于区间的求和,还能用于区间求最大值,最小值,最大公约数等。

能用于线段树的两个子节点的结果必须能合并,比如求和以及求最大值等,区间和=左子节点和+右子节点和,区间最大值= max(左子节点的最大值,右子节点的最大值)。

如果不能合并是没法使用线段树的,比如求众数,左子节点的众数和右子节点的众数可能不是一个,没法合并。这里用求“区间和”为例来介绍线段树,求区间的最大值和最小值原理基本上都一样,这里不在介绍。假设有一个长度为 10 的数组 [1,2,3,4,5,6,7,8,9,10] ,通过他来构建线段树,如下图所示。

75bdfd1bc51b6fb506e77c4b8d4f2043.png

我们看到叶子节点都存储的是原数组中的值,非叶子节点存储的是区间的和。在线段树中如果父节点的下标是 i ,他的左右两个子节点的下标分别为 i*2+1 和 i*2+2 。线段树中有两个数组,一个是原数组,一个是线段树数组。

int[] nums;// 原数组。
int[] trees;// 线段树数组,长度是原数组长度的4倍。

构建线段树

线段树是一棵平衡的二叉搜索树,构建的时候可以使用递归的方式构建。

// 调用方式 build(0, 0, nums.length - 1);
// 调用之前要先初始化nums和trees数组。
void build(int root, int left, int right) {
    if (left == right) {// 到叶子节点,直接对线段树数组赋值。
        trees[root] = nums[left];
    } else {
        // 递归构建左子树和右子树。
        int mid = (left + right) >>> 1;
        build(root * 2 + 1, left, mid);
        build(root * 2 + 2, mid + 1, right);
        // 类似于二叉树的后续遍历,子节点计算完之后在计算当前节点。
        pushUp(root);
    }
}

// 往上推。
void pushUp(int i) {
    // 求区间和,父节点的值等于左右子节点之和。
    trees[i] = trees[i * 2 + 1] + trees[i * 2 + 2];
    // 如果是求区间最大值可以这样写。
    // trees[i] = Math.max(trees[i * 2 + 1], trees[i * 2 + 2]);
}

单点查询

单点查询是从线段树的根节点开始往下查询,直到叶子节点,最后叶子节点的值就是我们要查找的结果。

// 单点查询。
int querySingle(int pos) {
    if (pos < 0 || pos >= nums.length)
        return -1;
    return querySingleHelper(0, 0, nums.length - 1, pos);
}

/**
 * @param root  当前节点
 * @param start 当前节点的左区间
 * @param end   当前节点的右区间
 * @param pos   要查询的值
 * @return
 */
int querySingleHelper(int root, int start, int end, int pos) {
    if (start == end)// 到叶子节点直接返回。
        return trees[root];
    int mid = (start + end) >>> 1;
    if (pos <= mid)// 在左子节点查找。
        return querySingleHelper(root * 2 + 1, start, mid, pos);
    // 在右子节点查找。
    return querySingleHelper(root * 2 + 2, mid + 1, end, pos);
}

单点修改

单点修改和单点查询类似,他是先找到叶子节点,最后在进行修改,修改完之后父节点值也会发生变动,所以还需要往上推,更改父节点的值,一直到根节点,如下图所示。

26e64ea958f2362db34b2d9661dee37f.png

我们看到子节点的值改了,父节点的值都要跟着改变。

// 单点更新,nums[pos]+=val
void updateSingle(int pos, int val) {
    if (pos < 0 || pos >= nums.length)
        return;
    updateSingleHelper(0, 0, nums.length - 1, pos, val);
}

void updateSingleHelper(int root, int start, int end, int pos, int val) {
    if (start == end) {// 已经到叶子节点了,直接更新。
        trees[root] += val;// 这里是相对值,是加,不是直接赋值。
    } else {
        int mid = (start + end) >>> 1;
        if (pos <= mid)// 目标位置在左边。
            updateSingleHelper(root * 2 + 1, start, mid, pos, val);
        else// 目标位置在右边。
            updateSingleHelper(root * 2 + 2, mid + 1, end, pos, val);
        pushUp(root);// 往上推,更新父节点的值。
    }
}

区间查询

区间查询会有下面 4 种情况。如果查找区间非常大,包含了节点区间,直接返回当前节点值即可。如果查找区间只在左子树,就在左子树查找,如果只在右子树,就在右子树查找,否则左右两个子树都要查,如下图所示。

2b97eab2b149f188cdeea704ca5e2b23.png

168d7d0c0c623e662f6a7583b7496d2d.png

假设查找 [2,5] 之间的和,查找步骤如下图所示。

e69bae578dc1cf52995bb1d59dcd383f.png

// 区间查询。
int queryRange(int left, int right) {
    return queryRangeHelper(0, 0, nums.length - 1, left, right);
}

int queryRangeHelper(int root, int start, int end, int left, int right) {
    // 当前节点在查找的区间之内,直接返回该节点的值。
    if (left <= start && right >= end)
        return trees[root];
    int mid = (start + end) >>> 1;
    int sum = 0;
    if (left <= mid)// 在左边查找。
        sum += queryRangeHelper(root * 2 + 1, start, mid, left, right);
    if (right > mid)// 在右边查找,注意这里没有else,因为查找区间可能两边都有。
        sum += queryRangeHelper(root * 2 + 2, mid + 1, end, left, right);
    return sum;
}

区间修改

区间修改可以参考单点修改,一直往下找到叶子节点,把它的值给修改,然后还要一直往上修改父节点值。区间修改不同于单点修改的地方在于区间修改可能两个子节点都要修改,就像区间查询一样。

// 区间修改,把区间[left,right]中所有的数字加上val。
void updateRange(int left, int right, int val) {
    updateRangeHelper(0, 0, nums.length - 1, left, right, val);
}

void updateRangeHelper(int root, int start, int end, int left, 
                       int right, int val) {
    if (start == end) {
        trees[root] += val;// 到叶子节点,值加上val。
    } else {
        int mid = (start + end) / 2;
        if (left <= mid)// 在左子节点修改。
            updateRangeHelper(root * 2 + 1, start, mid, left, right, val);
        if (right > mid)// 在右子节点修改,和查询一样,也是没有 else。
            updateRangeHelper(root * 2 + 2, mid + 1, end, left, right, val);
        pushUp(root);// 往上推,更新父节点的值。
    }
}

懒标记

我们看到区间修改的时候他会把每一个叶子节点都要修改,然后在往上更新父节点,实际上可以不这样做,如果某个子树的值全部要修改,只需要更改这棵子树的根节点即可,给他加个标记,然后这棵子树的子节点都不要修改了。就像过年别人给你压岁钱一样,这个钱不是直接给你,而是先给你妈,然后你妈在给你们。由于给的人太多(类似于区间的频繁修改),你妈说这个钱就不一一发给你们了,放我这保管,需要的时候在给你们。

假设我们需要给区间 [3,9] 中的所有节点都加上 10 ,当我们找到需要更新的节点区间 [3,4] 和 [5,9] 的时候,只需要修改他们的值就可以了,然后给他加个懒标记值,他们的子节点就不在修改了,但他们的父节点还是要更新,如下图所示。

f2e1e9fe5b482568e580201c5764161c.png

假如需要查询区间 [3,7] ,首先他会在 [0,4] 和 [5,9] 两个子节点中查找,节点 [0,4] 又会在节点 [3,4] 查找,而 [3,7] 区间包含 [3,4] 区间,可以直接返回区间 [3,4] 的值。而 [3,7] 区间不全部包含 [5,9] 区间,所以会到节点 [5,9] 的子节点去找,因为节点 [5,9] 有懒标记,所以在到子节点查找之前,懒标记必须下发到子节点,所以代码中多了一个懒标记数组 lazy 。

int[] nums;// 原数组。
int[] trees;// 线段树数组。
int[] lazy;// 懒标记数组。

我们来看下懒标记下发的代码,这里是区间求和,所以需要左右子节点的个数进行累加,如果是求最大值就不需要。

/**
 * 懒标记下发
 *
 * @param index  当前节点
 * @param lCount 当前节点左子节点的个数
 * @param rCount 当前节点右子节点的个数
 */
void pushDown(int index, int lCount, int rCount) {
    if (lazy[index] != 0) {// 懒标记不为0开始处理。
        trees[index * 2 + 1] += lCount * lazy[index];// 更新左子节点的值。
        trees[index * 2 + 2] += rCount * lazy[index];// 更新右子节点的值。
        lazy[index * 2 + 1] += lazy[index];// 左子节点懒标记累加。
        lazy[index * 2 + 2] += lazy[index];// 右子节点懒标记累加。
        lazy[index] = 0;//当前节点的懒标记清空。
    }
}

在来看下有懒标记的区间更新,当区间被完全覆盖的时候就不在往下走了,直接在当前节点上改,然后加上懒标记值。

void updateRangeHelper(int root, int start, int end, int left, int right, 
                       int val) {
    if (left <= start && right >= end) {// 当前节点全部被更新区域覆盖。
        trees[root] += val * (end - start + 1);// val*子节点的个数。
        lazy[root] += val;// 当前节点要加上懒标记。
    } else {
        int mid = (start + end) / 2;
        // 当前节点没有全部被[left, right]覆盖,说明只更新当前子树
        // 的一部分,需要把懒标记下发到当前节点的两个子节点中。
        pushDown(root, mid - start + 1, end - mid);
        if (left <= mid)// 在左子节点修改。
            updateRangeHelper(root * 2 + 1, start, mid, left, right, val);
        if (right > mid)// 在右子节点修改,和查询一样,也是没有 else。
            updateRangeHelper(root * 2 + 2, mid + 1, end, left, right, val);
        pushUp(root);// 往上推,更新父节点的值。
    }
}

在来看下有懒标记的区间查询。

int queryRangeHelper(int root, int start, int end, int left, int right) {
    // 当前节点在查找的区间之内,直接返回该节点的值。
    if (left <= start && right >= end)
        return trees[root];
    int mid = (start + end) >>> 1;
    // 查找区域没有把当前节点的区域覆盖,说明会到子节点中
    // 查找,需要把当前节点的懒标记下发到子节点中。
    pushDown(root, mid - start + 1, end - mid);
    int sum = 0;
    if (left <= mid)// 在左边查找。
        sum += queryRangeHelper(root * 2 + 1, start, mid, left, right);
    if (right > mid)// 在右边查找,注意这里没有else,因为查找区间可能两边都有。
        sum += queryRangeHelper(root * 2 + 2, mid + 1, end, left, right);
    return sum;
}

动态开点

线段树虽然是一棵平衡的二叉搜索树,但创建的时候并没有看到他的节点类,因为我们使用的是纯数组 trees ,类似于堆,但又不同于堆,因为堆可以看做是一棵完全二叉树,但线段树不是。这就导致了 trees 的长度是原数组长度的 4 倍,其中有很多空间是浪费没有存储的。

有的同学可能会说既然没有存储为啥还要申请那么大的空间,这里我们就来讲一下线段树数组 trees 为啥要申请 4 倍空间。比如我们用长度为 n 的数组来创建线段树,那么线段树中肯定有 n 个叶子节点,并且还有 n-1 个祖先节点( n 不等于 1 ,否则没有祖先节点),对于线段树的最后一行不一定都是完全填满的,最后一行的节点前面必须要有空间填充,极端情况下是 2*(n-2) 个节点,所以总共需要 4*n-5 个节点,如下图所示。

fa816d6732833de7c4d45545a7df15fd.png

既然使用纯数组会造成空间的极大浪费,我们可以使用另外一种方式,为每个节点创建一个实体类,这样最后一层就不需要填充了。但这样还不够,还可以继续优化,如果某些节点暂时没用到就不需要创建,只有在需要的时候才会创建,也就是动态创建节点,我们称为动态开点。具体可以看下《算法秘籍》的1.6.6 线段树,这里就不在过多介绍。

ee2e21e98f0a2a476786947c5f94bf54.gif

笔者简介

博哥,真名:王一博,毕业十多年,《算法秘籍》作者,专注于数据结构和算法的讲解,在全球30多个算法网站中累计做题2000多道,在公众号中写算法题解800多题,对算法题有自己独特的解题思路和解题技巧,喜欢的可以给个关注,也可以下载我整理的1000多页的PDF算法文档。

  • 19
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
公务员计算机网络试题主要涵盖计算机网络的基本概念、网络协议、网络安全等方面的知识。以下是一些常见的公务员计算机网络试题及其答案: 1. 什么是计算机网络? 计算机网络是指将多台计算机通过通信设备和通信线路连接起来,实现信息交换和资源共享的系统。 2. OSI参考模型有哪些层次?请简要介绍每个层次的功能。 OSI参考模型包括物理层、数据链路层、网络层、传输层、会话层、表示层和应用层。 - 物理层:负责传输比特流,主要涉及物理介质、电压等物理特性。 - 数据链路层:负责将比特流划分为帧,并进行错误检测和纠正。 - 网络层:负责数据包的路由选择和转发。 - 传输层:提供端到端的可靠数据传输,包括TCP和UDP协议。 - 会话层:负责建立、管理和终止会话。 - 表示层:负责数据的格式化、加密和压缩等操作。 - 应用层:提供用户与网络之间的接口,包括HTTP、FTP等协议。 3. TCP/IP协议族包括哪些协议? TCP/IP协议族包括IP协议、TCP协议、UDP协议、HTTP协议、FTP协议等。 4. 什么是IP地址和子网掩码? IP地址是用于标识网络中设备的唯一地址,分为IPv4和IPv6两种。子网掩码用于划分IP地址的网络部分和主机部分。 5. 什么是HTTP协议? HTTP协议是一种用于传输超文本的应用层协议,基于客户端-服务器模型,常用于Web浏览器和Web服务器之间的通信。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

数据结构和算法

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

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

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

打赏作者

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

抵扣说明:

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

余额充值