一文講解「線段樹」到底是什麼

2020-08-29 青峰科技

線段樹是一個複雜的數據結構,比較難理解,也比較難解釋清楚。在我將這個數據結構反覆學習了五遍的時候,我終於有了信心寫出這篇介紹線段樹的文章。希望大家能夠掌握這種數據結構。

這篇文章比較長,建議大家耐心閱讀,好好消化吸收哦~~

前置內容

學習線段樹前,你需要掌握二叉搜索樹,不太了解的小夥伴,可以看看小灰之前發布的紅黑樹漫畫,前半部分講解了二叉搜索樹:

我只補充一個內容,就是關於二叉搜索樹如何編號

二叉搜索樹的根節點編號為1,對於每個節點,假如其編號為N,它的左兒子編號為2N,右兒子編號為2N+1。因此,整個二叉搜索樹的編號如下:


上圖當中,結點上方的數字是結點的編號,後續為了簡單,把編號寫在結點內不。


有讀者可能要問了,為什麼3的兒子是6和7,而不是4和5呢?這是因為雖然節點4和節點5不存在,但是仍然應該為他們保留4和5這2個編號,你可以把這棵樹看成這樣:


線段樹的概念

線段樹,英文名稱是Segment Tree,其本質也是一個二叉搜索樹,區別在於線段樹的每一個節點記錄的都是一個區間,每個區間都被平均分為2個子區間,作為它的左右兒子。比如說區間[1,10],被分為區間[1,5]作為左兒子,區間[6,10]作為右兒子:


為什麼要設計這樣奇怪的數據結構呢?

線段樹主要適用於某些相對罕見的應用場景:

比如給定了若干元素,要求統計出不同區間範圍內,元素的個數。

現在我們已經知道了什麼是線段樹,那麼看一個利用線段樹的例子。

線段樹的存儲與建造

這是一個序列:

現在我們要用它完成一個區間求和的任務。

區間求和就是指求序列中一段區間的所有元素之和。比如說上面的序列,區間[1,5]的和為元素1+元素2+元素3+元素4+元素5,也就是14。再舉一個例子,區間[9,10]的和為9。

在學習線段樹的概念的時候,我們就知道線段樹的每個節點都存儲了一個區間。比如說對於[1,10]這個節點,也就是這棵線段樹的根節點,那麼它的值為1+5+1+3+4+2+0+9+0+9=34。看我們把這棵樹填完:

(當一個區間的左右邊界已經相等時,比如[1,1],表示這個區間內只有一個元素了,此時不能再分割,因此它就沒有左右兒子節點了)

現在就讓我們用代碼實現線段樹:

【代碼片段 1】 用一個類Node表示線段樹的節點:

class Node {     int l; // l是區間左邊界     int r; // r是區間右邊界     int sum; // sum是區間元素和     public Node (int l, int r, int sum){         this.l = l;         this.r = r;         this.sum = sum;     }}

【代碼解析 1】 線段樹的任意節點都有3個屬性:

  • 區間的左邊界l
  • 區間的右邊界r
  • 區間的元素和sum

比如說在上面的線段樹中,區間[1,10]這個元素:

  • 左邊界為1
  • 右邊界為10
  • 元素和為34

【代碼片段 2】 定義元素個數、原序列和線段樹

static int n = 10; // n是元素個數static int[] array = {0, 1, 5, 1, 3, 4, 2, 0, 9, 0, 9}; // array是原序列(第一個0是佔array[0]位的)static Node[] tree = new Node[4*n];static void initTree (){    for(int i = 0; i < tree.length; i++){        tree[i] = new Node(0, 0, 0, 0);    }}

【代碼解析 2】 首先我們在上文已經定義了元素個數和原序列。他們的值如下:

  • 元素個數為10個
  • 原序列為[0,1,5,1,3,4,2,0,9,0,9]

現在問題在於,存儲線段樹的數組應該開多大的空間?根據證明發現,一個有n個元素的序列,所對應的線段樹至少需要大小為4n的數組來存儲。這一類證明網上有很多,讀者可以自行查閱一下。

我們用inittree這個函數進行線段樹初始化(tree數組初始值為null,不初始化會報錯,我在這個地方卡了好久)

【代碼片段 3】 updateNode函數負責更新節點的值:

static void updateNode (int num) { // num是當前節點序號    tree[num].sum = tree[num * 2].sum + tree[num * 2 + 1].sum;}

【代碼解析 3】 仔細觀察前面的線段樹可以發現,每一個節點的值都等於其左右兒子值的和。我們剛剛學會,一個編號為n的節點,其左右兒子分別為2n和2n+1。因此我們把num的值更新為2num+2num+1,也就是其左右兒子的和。

【代碼片段 4】 build函數建造線段樹:

static void build (int l, int r, int num) { // 建樹    tree[num].l = l;    tree[num].r = r;    if (l == r) { // l = r說明到達葉子節點        tree[num].sum = array[l];        return;    }    int mid = (l + r) / 2;    build(l, mid, num * 2); // 遞歸左兒子    build(mid + 1, r, num * 2 + 1); // 遞歸右兒子    updateNode(num);}

【代碼解析 4】 函數從區間[l,r]開始遞歸遍歷整棵線段樹,每一次都遞歸它的左右兒子,到葉子節點時結束。遞歸每一個兒子時,都對它進行更新。這樣下來就完成了整棵樹的初始化。

線段樹的單點修改

現在假如我們需要把第6個元素從2修改為3:

那麼就會有很多的區間相應的改變。比如說區間[5,7],從4+2+0=6變成了4+3+0=7。現在讓我們手動模擬一下線段樹的單點修改過程。這裡假設我們需要把元素6從2變成3:

首先,從根節點開始遍歷,發現含有元素6的區間是根節點的右兒子,與左兒子沒有關係。因此將修改目標鎖定到右兒子:

第二步,發現含有6的區間是左兒子,因此把目標放到左兒子上:

第三步同理:

第四步同理:

此時發現這是一個葉子節點,因此對它進行更新,從2變成3:

返回到上一層:

接下去同理:

然後我們跳過演示,讀者可以自己試試看用同樣的方法修改這棵樹。最後修改完應該是這樣的:

根節點最後應該從34變成35,我經常會忘記修改它的值,大家千萬不要忘記修改它。

演示完以後我們分析一下時間複雜度。如果我們使用線段樹修改元素,每次都是折半操作,相當於二分查找的速度,時間複雜度僅僅是對數級別,也就是 。

【代碼片段 5】 modify函數實現單點修改:

static void modify (int i, int value, int num) { // 把元素i修改為值value    if (tree[num].l == tree[num].r) { // 到達葉子節點        tree[num].sum = value;        return;    }    int mid = (tree[num].l + tree[num].r) / 2;    if (i <= mid) {        modify(i, value, num * 2); // 遞歸左兒子    }    else {        modify(i, value, num * 2 + 1); // 遞歸右兒子    }    updateNode(num);}

【代碼解析 5】 這一段代碼也不是很難。每一次我們都從根開始遞歸遍歷。我們先判斷要更改的元素屬於當前節點的左兒子還是右兒子,並且遞歸到該節點。遞歸結束後更新當前節點的值。假如遍歷到葉子節點,說明我們已經遍歷到了想要修改的元素,那麼我們直接把該節點的值修改為value就可以了。

到這裡我們已經學會了單點修改的方法了。接下來讓我們更進一步,學習區間修改。

線段樹的區間修改

首先讓我們明確一下區間修改的概念:

單點修改,大致是以下兩個步驟:

  1. 找到需要修改的點
  2. 修改這個點

而區間修改是這樣兩個步驟:

  1. 找到需要修改的區間
  2. 修改這段區間內的所有點

好的,概念我們明白了,現在要知道如何實現這個功能。首先我們看一看區間修改可能的情況:

  1. 需要修改的區間包含在兒子之內:為大家畫個圖:我們看到需修改區間[6,8]包含在未修改區間的右兒子裡。這種情況很簡單,我們直接遞歸到右兒子即可。
  2. 需要修改的區間被拆開:還是畫一個圖:這時4屬於左兒子,但是5和6屬於右兒子。這怎麼辦呢?最直接的方法是把這個區間拆成兩半,屬於左兒子的放一邊,屬於右兒子的放一邊,像這樣:

兩種情況分類討論後,我們就要考慮如何修改區間了。

最簡單的方法就是把這些區間挨個兒修改。但是大家可以試試看,這種方法比暴力還要慢好幾倍。因此我們需要使用懶惰標記

現在假如我們需要把區間[5,7]每個元素增加2:

首先,5屬於根節點的左兒子,而6和7屬於根節點的右兒子,因此兩邊都要進行修改。我們可以先修改左兒子:

5屬於當前節點的右兒子,因此我們鎖定右兒子:

5屬於當前節點的右兒子,那麼我們修改右兒子。我們發現右兒子就是5。當前只有一個元素,因此我們把當前的值+2,並為其打上一個懶惰標記,懶惰標記的值也是2:

之後向上回溯,每一個節點都進行更新,也就是說每一個節點都更新為其左兒子+右兒子,最後更新完是這樣的:

到目前為止,懶惰標記還沒有發揮作用,但是我們可以看一看6和7這段區間的修改。首先因為6和7在根節點的右兒子,因此我們先遍歷右兒子:

接著因為6和7在當前節點的左兒子,因此我們遍歷左兒子:

之後我們發現6和7就是當前節點的左兒子,因此我們直接遍歷到左兒子,修改其值並打上懶惰標記。需要指出的是,因為6~7有2個元素,因此增加的值要乘2,也就是從+2變為+4,但懶惰標記的值不用乘2:

此時讓我們思考一個問題:

我們還需要遍歷修改[6,6]和[7,7]嗎?

這時就不用了,因為我們已經打上了懶惰標記,懶惰標記的初衷就是延遲修改,因此我們當然不需要再修改這兩個節點了。現在讓我們一鼓作氣,回溯到根節點,完成所有更新:

現在我們一起用代碼實現:

【代碼片段 6】 為Node類添加懶惰標記:

class Node {     int l; // l是區間左邊界     int r; // r是區間右邊界     int sum; // sum是區間元素和     int lazy; // lazy是懶惰標記     public Node (int l, int r, int sum, int lazy){         this.l = l;         this.r = r;         this.sum = sum;         this.lazy = lazy;     }}

【代碼解析 6】 新增了lazy變量作為懶惰標記。

【代碼片段 7】 modifySegment函數實現區間修改的代碼:

static void modifySegment(int l, int r, int value, int num) { // [l,r]每一項都增加value    if (tree[num].l == l && tree[num].r == r) { // 找到當前區間        tree[num].sum += ( r - l + 1 ) * value; // r-l+1是區間元素個數        tree[num].lazy += value;        return;    }    int mid = (tree[num].l + tree[num].r) / 2;    if (r <= mid) { // 在左區間        modifySegment(l, r, value, num * 2);    }    else if (l > mid) { // 在右區間        modifySegment(l, r, value, num * 2 + 1);    }    else { // 分成2塊        modifySegment(l, mid, value, num * 2);        modifySegment(mid + 1, r, value, num * 2 + 1);    }    updateNode(num);}

【代碼解析 7】 首先,按照開始講的3種情況,進行分類討論(情況分別是:完全在左區間,完全在右區間,分成了2塊),並且向下遞歸。

線段樹的區間查詢

區間查詢,顧名思義就是查詢一段區間內的元素和。那麼如何實現呢?

不急,現在我們來看這樣一種情況:

[1,2]有一個懶惰標記2。現在假如我要求[1,1]的值怎麼辦?

涼拌

為什麼我這麼說?因為[1,2]這個節點有一個懶惰標記,但是[1,1]卻沒有被更新,這是一個問題。

此時我們就要實現一個函數,用於把懶惰標記下傳給兒子們,稱為pushdown函數。下面直接給代碼,解析部分請看代碼解析吧:

【代碼片段 8】 使用pushdown函數下傳懶惰標記:

static void pushdown (int num) {    if(tree[num].l == tree[num].r) { // 葉節點不用下傳標記        tree[num].lazy = 0; // 清空當前標記        return;    }    tree[num * 2].lazy += tree[num].lazy; // 下傳左兒子的懶惰標記    tree[num * 2 + 1].lazy += tree[num].lazy; // 下傳右兒子的懶惰標記    tree[num * 2].sum += (tree[num * 2].r - tree[num * 2].l + 1) * tree[num].lazy; // 更新左兒子的值    tree[num * 2 + 1].sum += (tree[num * 2 + 1].r - tree[num * 2 + 1].l + 1) * tree[num].lazy; // 更新右兒子的值    tree[num].lazy=0; // 清空當前節點的懶惰標記}

【代碼解析 8】 下傳懶惰標記步驟有3步:

  1. 將懶惰標記傳遞給兒子
  2. 更新兒子的值
  3. 清空當前節點的懶惰標記

需要注意的是,葉子節點不用下傳標記。

現在我們完成了pushdown函數的編寫,可以開始學習區間查詢了。剛才我們完成了區間修改,並且將原序列修改為了[1,5,1,3,6,4,2,9,0,9]。現在我們接著實現區間查詢問題。假如我們要查詢區間[5,6]:

正如我們所見,答案為10。現在告訴大家一個好消息,那就是區間查詢的大致步驟其實和區間修改沒有什麼出入。讓我們來實踐一下:

首先,5和6分別屬於根節點的左兒子和右兒子,那我們先遍歷左兒子:

接著繼續往下:

往下查找到[5,5]:

記錄好這邊答案為6。接著我們看根節點的右兒子,查找元素6:

向下搜索到[6,8]:

搜索到[6,7]:

此時我們需要下傳[6,7]的懶惰標記,並且更新[6,6]的值,如下:

最後遍歷到[6,6],值為4,與剛才得到的6相加,答案就是10:

那麼我們上代碼:

【代碼片段 9】 query函數實現區間查詢:

static int query (int l, int r, int num) {    if (tree[num].lazy != 0) { // 下傳懶惰標記        pushdown(num);    }    if (tree[num].l == l && tree[num].r == r) { // 找到當前區間        return tree[num].sum;    }    int mid = (tree[num].l + tree[num].r) / 2;    if (r <= mid) { // 在左區間        return query(l, r, num * 2);    }    if (l > mid) { // 在右區間        return query(l, r, num * 2 + 1);    }    return query(l, mid, num * 2) + query(mid + 1, r, num * 2 + 1); // 分成2塊}

【代碼解析 9】 步驟與區間修改完全相同,記得要pushdown一下就行。

思考與探究

下面讓我們進行一些對於線段樹的思考與探究:

【思考 1】 線段樹都應用於什麼環境?除了區間和外,能否解決更多問題?比起別的樹有什麼優勢?

【答案 1】 線段樹一般多用於區間問題。在本文中我們解決的是區間和,但是也能解決更多的問題,比如區間平方和等等。線段樹只能解決符合下麵條件的問題:

當區間[l,r]可以由[l,mid(l,r)]和[mid(l,r) + 1,r]得到答案

我們舉幾個滿足條件的例子:

  • 區間[5,8]的區間和,可以由[5,6]的區間和加上[7,8]的區間和得到。
  • 區間[5,8]的最小值,等於區間[5,6]的最小值與[7,8]的最小值的最小值。

但是還有一些不滿足條件:

  • 區間[5,8]的最長上升子序列。

另外就是線段樹比起別的樹的特點。線段樹屬於二叉搜索樹,像我們熟悉的紅黑樹AVL樹其實也都屬於二叉搜索樹。只不過不同的二叉搜索樹用處不相同。線段樹比起別的樹,它的最大特點就是用作存儲區間的特性。

【思考 2】 線段樹和前綴和算法有什麼優劣區別嗎?

【答案 2】 寫到這裡並不清楚各位是否明白前綴和算法。這裡給大家簡單介紹一下:

對於任何一個序列,都能製作一個相對應的前綴和數組。對於一個序列來講,假如我們用pre表示前綴和數組,那麼pre[i]就表示區間[1,i]的區間和,比如pre[3]為array[1]+array[2]+array[3],也就是7。

現在我們可用pre[i]表示區間[1,i],那麼假如有一個任意區間[l,r],我們應該怎麼表示它的區間和呢?仔細思考一下不難發現,區間[l,r]的區間和其實就是區間[1,r]減去區間[1,l - 1],剩下的也就是區間[l,r]了。因此我們可用pre[r]-pre[l-1]表示。

舉個例子,區間[3,5]的和為1+3+4=8,相當於區間[1,5]減去區間[1,(3 - 1)],也就是14-6=8。

我們發現,使用前綴和只要做一個減法就能得到區間和,而線段樹還要遍歷好多次,那是不是說,前綴和甚至要快於線段樹呢?我們可以來對比一下線段樹和前綴和的時間複雜度:

算法名稱初始化修改查詢前綴和O(n)O(n)O(1)線段樹O(log n)O(log n)O(log n)

我們發現,線段樹比起前綴和有更加穩定的特點。它的每一項都是對數級別。而前綴和雖然查詢非常快,但是修改速度就相對慢很多。因此我們認為,假如不需要進行元素的修改操作,那麼我們一般選擇前綴和。如果需要進行元素修改操作,那麼線段樹更為合適

線段樹的完整代碼

最後,附上線段樹的完整代碼實現:

static int n = 10; // n是元素個數static int[] array = {0, 1, 5, 1, 3, 4, 2, 0, 9, 0, 9};// array是原序列(第一個0是佔array[0]位的)static Node[] tree = new Node[4*n]; // tree是線段樹public static void main(String[] args) {    initTree();    build(1, 10, 1); // 利用build函數建樹    System.out.println(&34; + query(2, 5, 1));    // 利用query函數搜索區間和    modify(5, 9, 1); // 利用modify函數實現單點修改(元素5從4改為9)    System.out.println(&34; + query(2, 5, 1));    modifySegment(3, 4, 3, 1);    // 利用modifySegment函數將[3,4]每個元素增加3    System.out.println(&34; + query(2, 5, 1));}static void initTree (){    for(int i = 0; i < tree.length; i++){        tree[i] = new Node(0, 0, 0, 0);    }}static void updateNode (int num) { // num是當前節點序號    tree[num].sum = tree[num * 2].sum + tree[num * 2 + 1].sum;}static void build (int l, int r, int num) { // 建樹    tree[num].l = l;    tree[num].r = r;    if (l == r) { // l = r說明到達葉子節點        tree[num].sum = array[l];        return;    }    int mid = (l + r) / 2;    build(l, mid, num * 2); // 遞歸左兒子    build(mid + 1, r, num * 2 + 1); // 遞歸右兒子    updateNode(num);}static void modify (int i, int value, int num) { // 把元素i修改為值value    if (tree[num].l == tree[num].r) { // 到達葉子節點        tree[num].sum = value;        return;    }    int mid = (tree[num].l + tree[num].r) / 2;    if (i <= mid) {        modify(i, value, num * 2); // 遞歸左兒子    }    else {        modify(i, value, num * 2 + 1); // 遞歸右兒子    }    updateNode(num);}static void modifySegment(int l, int r, int value, int num) { // [l,r]每一項都增加value    if (tree[num].l == l && tree[num].r == r) { // 找到當前區間        tree[num].sum += ( r - l + 1 ) * value; // r-l+1是區間元素個數        tree[num].lazy += value;        return;    }    int mid = (tree[num].l + tree[num].r) / 2;    if (r <= mid) { // 在左區間        modifySegment(l, r, value, num * 2);    }    else if (l > mid) { // 在右區間        modifySegment(l, r, value, num * 2 + 1);    }    else { // 分成2塊        modifySegment(l, mid, value, num * 2);        modifySegment(mid + 1, r, value, num * 2 + 1);    }    updateNode(num);}static void pushDown(int num) {    if(tree[num].l == tree[num].r) { // 葉節點不用下傳標記        tree[num].lazy = 0; // 清空當前標記        return;    }    tree[num * 2].lazy += tree[num].lazy; // 下傳左兒子的懶惰標記    tree[num * 2 + 1].lazy += tree[num].lazy; // 下傳右兒子的懶惰標記    tree[num * 2].sum += (tree[num * 2].r - tree[num * 2].l + 1) * tree[num].lazy; // 更新左兒子的值    tree[num * 2 + 1].sum += (tree[num * 2 + 1].r - tree[num * 2 + 1].l + 1) * tree[num].lazy; // 更新右兒子的值    tree[num].lazy=0; // 清空當前節點的懶惰標記}static int query (int l, int r, int num) {    if (tree[num].lazy != 0) { // 下傳懶惰標記        pushDown(num);    }    if (tree[num].l == l && tree[num].r == r) { // 找到當前區間        return tree[num].sum;    }    int mid = (tree[num].l + tree[num].r) / 2;    if (r <= mid) { // 在左區間        return query(l, r, num * 2);    }    if (l > mid) { // 在右區間        return query(l, r, num * 2 + 1);    }    return query(l, mid, num * 2) + query(mid + 1, r, num * 2 + 1); // 分成2塊}static class Node {    int l; // l是區間左邊界    int r; // r是區間右邊界    int sum; // sum是區間元素和    int lazy; // lazy是懶惰標記    public Node (int l, int r, int sum, int lazy){        this.l = l;        this.r = r;        this.sum = sum;        this.lazy = lazy;    }}

相關焦點

  • ACMer不得不會的線段樹,究竟是種怎樣的數據結構?
    當然也有可能遇到面試官自己不會,為了防止尷尬強行讓你用非線段樹的解法來完成,比如我就遇到過……例題說了這麼多廢話,那麼線段樹究竟是什麼呢?線段樹的英文是segment tree,其實也算是一個直譯。我們先放一放,先來看一道例題,來實際體會一下,為什麼需要線段樹這個數據結構,以及它的使用場景究竟是什麼。這樣我們可以對它有一個更加直觀的感受,這道題很簡單也很經典,我就是在這道題遇到了面試官不讓用線段樹的突然襲擊。這道題的題面是這樣,給定一個長度為n的數組。這個數組當中有n個整數,然後我們會有兩種操作。
  • 小學六年級學生寫的 「線段樹」解析,厲害了! - CSDN
    線段樹主要適用於某些相對罕見的應用場景:比如給定了若干元素,要求統計出不同區間範圍內,元素的個數。現在我們已經知道了什麼是線段樹,那麼看一個利用線段樹的例子。線段樹的存儲與建造這是一個序列:現在我們要用它完成一個區間求和的任務。區間求和就是指求序列中一段區間的所有元素之和。
  • 高性能網絡編程(七):到底什麼是高並發?一文即懂
    :一文讀懂高性能網絡編程中的線程模型》《高性能網絡編程(七):到底什麼是高並發?那麼我們在談論高並發的時候,究竟在談什麼東西呢?歸根結底,到底什麼是高並發?想要深入地探討這個問題,本系列的第一篇文章可以詳讀一下:《高性能網絡編程(一):單臺伺服器並發TCP連接數到底可以有多少》。
  • 英語詞彙講解:obtain和attain到底有什麼區別?
    新東方網>英語>英語學習>語法詞彙>詞彙指導>正文英語詞彙講解:obtain和attain到底有什麼區別? ② 本網未註明"稿件來源:新東方"的文/圖等稿件均為轉載稿,本網轉載僅基於傳遞更多信息之目的,並不意味著贊同轉載稿的觀點或證實其內容的真實性。如其他媒體、網站或個人從本網下載使用,必須保留本網註明的"稿件來源",並自負版權等法律責任。如擅自篡改為"稿件來源:新東方",本網將依法追究法律責任。
  • 5G到底是什麼東西 5G網速有多快?一文看懂5G
    5G到底是什麼東西 5G網速有多快?一文看懂5G 5G火車站在上海虹橋火車站啟動建設。而根據三大運營商的時間規劃,5G設備將於今年開始試商用,手機廠商也正在加速推出5G手機。
  • 一文讀懂響應式編程到底是什麼?
    ,節選自《Java編程方法論:響應式Spring Reactor 3設計與實現》一書響應式編程到底是什麼?所以這個過程其實就是下發產生的事件,然後我們作為消費者對下發事件進行一系列的消費。從這個角度來說,對整個代碼的設計應該是針對消費者來進行的。比如,看電影,有些畫面我們不想看,那就閉上眼睛;有些聲音不想聽,那就捂上耳朵。
  • 網絡編程懶人入門(十一):一文讀懂什麼是IPv6
    然而幾次下來,到底什麼是IPv6,還是有點雲裡霧裡。那麼,IP協議在TCP/IP體系中到底有多重要?看看下圖便知(原因清晰版:從此處進入下載)。《網絡編程懶人入門(十):一泡尿的時間,快速讀懂QUIC協議》《網絡編程懶人入門(十一):一文讀懂什麼是IPv6》(本文)3、複習一下什麼是IPv4?
  • 一文講透aPaaS平臺是什麼
    aPaaS到底是什麼意思,有什麼用,與前三者的區別是什麼?本文將對這些問題進行徹底探討。什麼是雲計算在探討什麼是aPaaS之前,我們有必要講解一下雲計算的概念。想像一下,假設你要開發一款軟體程序,會需要用到哪些技術或設施呢?
  • 「說文解字」又開課了!章必功先生帶來《紅樓夢》趣味講解
    讀創/深圳商報記者 祁琦11月23日上午,由深圳市雜文學會承辦的「說文解字:社會主義核心價值觀與中華古詩文公益課堂」系列講座,在南山區蛇口街道辦事處文化站成功舉辦了首場。深圳大學原校長章必功先生帶來了《紅樓夢》的趣味講解。
  • 雙85電容是什麼意思?一文講透這個問題
    電容器的種類有很多,但對於雙85電容,不少人確實搞不懂,到底什麼是雙85電容呢?是一類電容器,還是很多類電容器的統稱呢?今天小編就徹底好好講解一下這個問題,讓大家對雙85電容了解透徹。雙85電容是什麼意思?
  • 一文總結:豬、牛、羊、雞鴨肉嫩不柴和除腥竅門,講解清楚漲知識
    但要把肉類做得滋味可口,對烹飪方法以及細節操作都有講究,因為肉類在烹飪過程中,會被一些微妙操作變化而改變肉菜成品口感,而且肉類都會帶有肉羶味,如果擔心對肉類菜式做不好,下面小鹿分享的「肉類處理乾貨」就幫到你了,一文總結:豬、牛、羊、雞鴨肉嫩不柴和除腥竅門,講解清楚漲知識,學會的朋友,
  • 一文看懂網際網路的"商業模式"到底是什麼?
    那麼我們總說的商業模式到底是什麼呢?答案:商業模式是由五個部分而組成的一個整體,分別是獲客渠道、銷售策略、收入模型、產品類型和交付方式。想了解關於KPI和電商營銷策略的話題可以回顧我之前的兩篇文章:制定網際網路產品KPI 的訣竅是什麼?為什麼理解KPI最好的方式竟然是「矽膠玩具」?電商的營銷策略和商業模式的秘密都藏在這一個數據後面在討論商業模式之前,我們先來想一想商業模式所要達到的目標是什麼?
  • 一看就懂,詳細講解鏡頭的焦距到底是什麼
    一、焦距是什麼?在鏡頭上會有很多標識,其中就有焦距的標識。簡單來說焦距就是鏡頭的眼睛,決定了取景的範圍。給大家舉個例子:在鏡頭上基本都會有這樣的標識「Canon EF-S 18-135mm f/3.5-5.6 IS」,其中18-135mm就代表了鏡頭的焦距,這兩個數字表示這個鏡頭的焦距是從70mm開始的,到200mm結束。
  • 到底什麼是Wi-Fi 6?讓你一文看懂它的優勢所在
    那今天跟大家講解下到底什麼是Wi-Fi 6,並且還都有哪些優勢。Wi-Fi相信大家都非常熟悉,每天都在用,但其實這只是一個無線網絡的商標,表示的是一項基於IEEE 802.11標準的無線區域網技術。
  • 一文搞懂到底什麼叫數據產品?
    今天的上篇主要聊聊這類數據產品的定位,下篇分享BI報表在AI化方向上的探索本文主要結構如下:1,從一個反例入手,討論數據產品的重心,到底該在數據還是在產品然而,作為一款廣義的數據產品,這個平臺上線半年後門庭冷落車馬稀......這裡給大家一張圖的時間停頓思考下,究竟是哪裡出了問題?
  • 一文讀懂三星Galaxy Note20 Ultra帶火的UWB技術到底是什麼?
    一文讀懂三星Galaxy Note20 Ultra帶火的UWB技術到底是什麼? 什麼是UWB?
  • 二年級上冊口語交際做手工,詳細的講解寫作思路,文末附範文
    有趣的動物,二年級口語交際寫作講解,文末附150字精彩範文2、告訴同學你做的是什麼?3、是怎樣做的?在上面圖片的右下角還有一個黃色的小方框,方框例第一要求:按照順序說,也是很重要的。我們先賣個關子,在下文中重點講解。
  • 一文輕鬆了解vlan埠模式Access,Trunk,Hybird,通信理論
    在上一篇中介紹了一文輕鬆理解vlan虛擬區域網到底是什麼,圖解vlan流程本節內容主要對vlan埠模式認識,接下來的章節會對二層通信知識點進行系統講解。vlan配置命令:創建VLANcreate vlan <1-4094>刪除VLANno vlan <1-4094>設置VLAN活動狀態state {active | suspend}#通信#,#二層#,#vlan#,#Access#,#Hybrid#,#Trunk##上一章
  • 談談模擬器和安卓手機到底有什麼區別?
    前言:本文由花生選自360互動新媒體網微信營銷板塊,是微營銷設備講解文章的第一篇,第二篇是「做微營銷為什麼說蘋果手機要比安卓好很多?」