數據結構:線段樹入門與實踐

2021-03-02 FluentStudy

最近在刷題的時候,遇到一個涉及到線段樹的問題。之前沒接觸過,看了幾遍題解才看懂。這裡簡單介紹下入門的過程。

高級數據結構,線段樹入門

一、線段樹的基本思想

線段樹是一種常用來維護區間信息的數據結構,它適用於對區間內進行單點查詢、更新、求最值等操作,且時間複雜度能控制到 O(logN)。它的構建過程用到了二分的思想,通過不斷的二分將區間分成兩段,並分別對應左孩子和右孩子。

下面舉例來說明:比如有一個數組 [1, 2, 5, 7, 8, 10,12,18],它的長度是 8,所以範圍是 [1, 8]。如果用二分的思想來分解構造出的線段樹如下所示:

接下來我們來看看怎麼定義線段樹的數據結構。通常有兩種方式,一種方式是定義一個 class,一種方式是使用連續的數組。首先我們來看下自定義 class 的方式,這裡使用 Python 代碼:

class SegTree:
    def __init__(self, left, right):
        # 當前結點的左邊界
        self.lo = left
        # 當前結點的右邊界
        self.hi = right
        # 記錄額外的信息,這裡通常可以是最值或是區間和,根據題目需求來定義
        self.other_inf = 0
        # 左、右孩子
        self.left = None
        self.right = None

這種定義方式比較直接,但遍歷起來稍麻煩一點。而第二種方式是使用連續的數組。從上圖我們構造的線段樹可以看出,拋開葉子結點,樹是一個滿二叉樹,所以可以使用連續的數組來存儲,且父子結點的關係為

parent[i]
parent.left = parent[i*2]
parent.right = parent[i*2+1]

# 對於 i * 2 和 i*2+1,使用位運算可快速得到
i * 2 = i << 1
i * 2 + 1 = i << 1 | 1

使用數組時需要注意,葉子結點其實就是對應的給定數組的值,但數組的長度不一定能滿足滿二叉樹葉子結點的個數,這個時候代碼編寫上就比較靈活了,一般有兩種方式:

這種思路的其實相對比較好理解,因為給定的數組都需要放到葉子結點,那如果想要樹是一棵滿二叉樹,則葉子結點的個數必須是 2^n。所以我們需要找到第一個大於等於數組長度的 2 的 n 次冪。對於求第一個大於等於數組長度的 2 的 n 次冪的方法有很多,通過幾個位運算就能實現的,可以參考 Java HashMap 的源碼,也可以看 Integer 的 highestOneBit 方法,代碼如下(這裡不解釋具體原因):

public static int highestOneBit(int i) {
        // HD, Figure 3-1
        i |= (i >>  1);
        i |= (i >>  2);
        i |= (i >>  4);
        i |= (i >>  8);
        i |= (i >> 16);
        return i - (i >>> 1);
}

而用一個我們比較好理解的方法,如下:

n = 1
while n < len(nums):
   n <<= 1

找到這個數值之後,就可以進行初始化:

# 因為 n 是第一個大於或等於 len(nums) 的 2 次冪,它是等於它之前所有結點和 + 1 的
# 而一般在線段樹中第 0 位通常不用
# 因此 [0] * n 即所有非葉結點的初始化
# [nums] 則是初始化數組,並將其分配到葉子結點
# [0] * (n - len(nums)) 葉子結點未被分配到值的用 0 補全
self.seg_tree = [0] * n + nums + [0] * (n - len(nums))
# 初始化賦值, 根據父子關係的公式
for k in range(n - 1, 0, -1):
    self.seg_tree[k] = self.seg_tree[2 * k] + self.seg_tree[2 * k + 1]

# 這裡的做法參考:https://leetcode-cn.com/problems/range-sum-query-mutable/solution/python-shu-zhuang-shu-zu-binary-indexed-tree-by-ze/

因為如果長度為 n 的數組都需要放到葉子結點上,則它的上層有 n/2 個結點,再上層 n/4…,根據等比求和公式很容易得出所有結點個數一定小於 2n。所以我們整個線段樹數組的值設置為 2n 就足夠使用了。代碼如下:

n = len(nums)
self._n = n
self._tree = [0] * (n << 1)
# 將最後 n 位放置到葉子結點,也就是數組的最後 n 位
for i in range(n, len(self._tree)):
    self._tree[i] = nums[i - n]

for i in range(n - 1, 0, -1):
    # 父結點 = 左結點(父結點序號*2) + 右結點(父結果序號*2+1)
    self._tree[i] = self._tree[i << 1] + self._tree[i << 1 | 1]

此外,線段樹常用的方法有:

# 將第 i 個位置的數值更新為 val
def update(i, val)
# 將第 i 個位置的數值加上 val
def add(i, val)
# 查詢區間[i, j]上的區間和或最值,根據具體需求來具體分析
def query(i, j)

這裡就不一一實現這些方法,通過兩個題目來具體實踐下。

二、實踐

首先來看一個簡單點的題目:

給定一個整數數組  nums,求出數組從索引 i 到 j  (i ≤ j) 範圍內元素的總和,包含 i, j 兩點。

update(i, val) 函數可以通過將下標為 i 的數值更新為 val,從而對數列進行修改。

示例:

Given nums = [1, 3, 5]

sumRange(0, 2) -> 9
update(1, 2)
sumRange(0, 2) -> 8
說明:

數組僅可以在 update 函數下進行修改。
你可以假設 update 函數與 sumRange 函數的調用次數是均勻分布的。

來源:力扣(LeetCode)
連結:https://leetcode-cn.com/problems/range-sum-query-mutable
著作權歸領扣網絡所有。商業轉載請聯繫官方授權,非商業轉載請註明出處。

這個題目其實很簡單,首先拋開線段樹的思想,其實直接通過 Python list 就可以實現,也是可以 AC 的:

from typing import List
class NumArray:
    def __init__(self, nums: List[int]):
        if not nums:
            self._nums = []
        else:
            self._nums = nums

    def update(self, i: int, val: int) -> None:
        if i >= len(self._nums) or i < 0:
            return
        self._nums[i] = val

    def sumRange(self, i: int, j: int) -> int:
        return sum(self._nums[i:j + 1])

那如果使用線段樹呢?這裡我們使用數組存儲的方式來實現。首先線段數數組初始化可以直接套用上面說到的兩種方式,關鍵是 update 與 sumRange。而對於上面提到的兩種方式其實 update 和 sumRange 在解決的的時候實際代碼是一樣的。這裡我們以第二種方式初始化方式為例(畢竟會減少空間的消耗):

class NumArray:
    def __init__(self, nums: List[int]):
        if not nums:
            self._tree = []
            return
        n = len(nums)
        self._n = n
        self._tree = [0] * (n << 1)
        # 將最後 n 位放置到葉子結點,也就是數組的最後 n 位
        for i in range(n, len(self._tree)):
            self._tree[i] = nums[i - n]

        for i in range(n - 1, 0, -1):
            # 父結點 = 左結點(父結點序號*2) + 右結點(父結果序號*2+1)
            self._tree[i] = self._tree[i << 1] + self._tree[i << 1 | 1]
        # print(self._tree)

接下摟我們來看下 update 方法:

def update(self, i: int, val: int) -> None:
    if i < 0 or i >= self._n:
        return
    # 更新數組的第 i 個位置,即更新 self._tree 的第 n + i 個位置,i 是從 0 開始
    # 記錄和改變了多少
    change = val - self._tree[self._n + i]
    self._tree[self._n + i] = val
    # 更新 n+i 結點的所有父結點
    parent = (self._n + i) // 2
    while parent:
        self._tree[parent] += change
        parent //= 2
    # print(self._tree)

update 方法其實思路也比較簡單,先去更新葉子結點上的數值,並記錄改變差值,然後依次更新父結點的記錄和。我們再來看下 sumRange:

def sumRange(self, l: int, r: int) -> int:
    # 做一些特殊邊界情況判斷
    if l > r:
        return 0
    if r < 0:
        return 0
    if l > self._n:
        return 0
    if r < 0:
        i = 0
    if l >= self._n:
        j = self._n - 1

    l += self._n
    r += self._n
    result = 0
    # 當 l <= r 時
    while l <= r:
        # 如果左邊界是右孩子,則說明不能加它的父結點的值,所以它的值需要單獨加
        if l % 2 == 1:
            result += self._tree[l]
            # 加完之後,l向後移動,則移到了父結點右孩子的左孩子結點
            l += 1
        # 如果右邊界在左孩子,則左孩子需要單獨加
        if r % 2 == 0:
            result += self._tree[r]
            r -= 1
        l //= 2
        r //= 2
    return result

我個人認為 sumRange 比 update 要難理解一點,它的主要思想在於如果當前要求值的範圍比當前結點記錄的範圍要大(即既需要左孩子,也需要右孩子),則找父結點,如果只需要當前結點,就加上當前結點。

至此,這個題目就解決了。使用數組的話,代碼在理解上會複雜一點,主要是要對父子關係的靈活運用。

接下來,我們來看另外一個題目,我也是在刷這個題目時了解到線段樹這個數據結構:

給定一個整數數組 nums,返回區間和在 [lower, upper] 之間的個數,包含 lower 和 upper。
區間和 S(i, j) 表示在 nums 中,位置從 i 到 j 的元素之和,包含 i 和 j (i ≤ j)。

說明:
最直觀的算法複雜度是 O(n2) ,請在此基礎上優化你的算法。

示例:

輸入: nums = [-2,5,-1], lower = -2, upper = 2,
輸出: 3 
解釋: 3個區間分別是: [0,0], [2,2], [0,2],它們表示的和分別為: -2, -1, 2。

來源:力扣(LeetCode)
連結:https://leetcode-cn.com/problems/count-of-range-sum
著作權歸領扣網絡所有。商業轉載請聯繫官方授權,非商業轉載請註明出處。

這個題目在分析時,我們需要將式子做一個轉換,一旦做了這個轉換,基本就成功一半了。轉換關係如下:

求:lower <=  sum(i, j) <= upper
而 sum(i, j) = nums[0] + nums[1] + ... nums[j] - (nums[0] + nums[1] + ... + nums[i-1])
即 sum(i, j) = prefixSum[j] - prefixSum[i-1] , 這裡 prefixSum 為 nums 的前綴和數組

所以題目可以轉換為求:
lower <=  prefixSum[j] - prefixSum[i-1] <= upper

-> 

# lower + prefixSum[i-1] <= prefixSum[j] <= upper + prefixSum[i-1]
# 或是
# prefixSum[j] - upper <= prefixSum[i-1] <= prefixSum[j] - lower

# 如果是第一種移動方法,則表示當給定一個 i 位置的前綴和時,需要找從 i+1 位置往後,滿足前綴和在 lower + prefixSum[i] 和 upper + prefixSum[i] 的個數
# 如果是第二種移動方法,則表示當給定一個 j 位置的前綴各時,需要找從 0 到 j-1 位置的前綴各,在 prefixSum[j] - upper 和 prefixSum[j] - lower 範圍內的

當我們分析到這一步時,我們可以發現,其實我們要求的就是當給定一個數值,然後求在一個範圍內的數值中,在指定範圍的數值有多少個。比如題目中的例子:nums = [-2,5,-1], lower = -2, upper = 2,前綴和數組為 [-2, 3, 2],因此我們可以遍歷前綴和數組,如果是從後往前遍歷,則是利用 lower + prefixSum[i-1] <= prefixSum[j] <= upper + prefixSum[i-1] 這個轉換,如果是從前往後遍歷,則是使用 prefixSum[j] - upper <= prefixSum[i-1] <= prefixSum[j] - lower 轉換(主要是需要確定範圍,所以固定的是式子左右兩邊的變量)。

不管使用哪種方式,其實思路都是一樣的。我們首先找到一個基準的前綴和,然後從當前這個基準向前(或向後)找在範圍內的個數,找到之後,將當前這個基準加入到某種數據結構中,在這個數據結構裡記錄的就是當前基準以前所有的前綴和。而且我們需要這個數據結構來保證,在這個數據結構中查詢在指定範圍內的數值個數時,性能很高,此外因為還會不斷做插入,也要保證插入的性能。

因此,我們明確了,解題需要前綴和數組,和一個能在區間內快速做查詢和插入(也可以是更新)的數據結構。顯然和我們線段樹的適用範圍是很相似的。直接看代碼吧。

# 使用線段樹,第一種移動方式,即 lower + prefixSum[i-1] <= prefixSum[j] <= upper + prefixSum[i-1]
class SegTree:
    def __init__(self, left, right):
        # 當前結點的左邊界
        self.lo = left
        # 當前結點的右邊界
        self.hi = right
        # 記錄在當前範圍內的數有多少個
        self.count = 0
        # 左、右孩子
        self.left = None
        self.right = None


class Solution:
    def buildSegTree(self, left: int, right: int) -> SegTree:
        # 感覺也可以用數組來代替
        node = SegTree(left, right)
        if left == right:
            return node
        mid = (left + right) // 2
        # 左邊一半
        left = self.buildSegTree(left, mid)
        # 右邊一半
        right = self.buildSegTree(mid + 1, right)
        node.left = left
        node.right = right
        return node

    def countOfSegTree(self, node: SegTree, left: int, right: int) -> int:
        """統計在線段樹中,在 [left, right] 範圍內的數值
        """
        # 如果範圍比當前 lo hi 的範圍大,則直接返回 count 值
        if left <= node.lo and node.hi <= right:
            return node.count
        # 如果沒在當前範圍內
        if left > node.hi or right < node.lo:
            return 0
        return self.countOfSegTree(node.left, left, right) + self.countOfSegTree(node.right, left, right)

    def insertToSegTree(self, node: SegTree, val: int) -> None:
        """在線段樹中,插入一個 val 的值
        """
        node.count += 1
        if node.lo == node.hi == val:
            return
        mid = (node.lo + node.hi) // 2
        if val <= mid:
            self.insertToSegTree(node.left, val)
        else:
            self.insertToSegTree(node.right, val)

    def countRangeSum(self, nums: List[int], lower: int, upper: int) -> int:
        if not nums:
            return 0

        # 第一步,求前綴和,注意第一個元素為0,這樣在遍歷時,第一個元素到最後一個元素的情況也會考慮進去
        prefix_sum = [0]
        for n in nums:
            prefix_sum.append(prefix_sum[-1] + n)
        # 第二步,求出所有 lower + prefixSum[i-1] 和 upper + prefixSum[i-1],以及 prefix_sum 本身
        allNumbers = set()
        for n in prefix_sum:
            allNumbers.add(n)
            allNumbers.add(lower + n)
            allNumbers.add(upper + n)
        # 將 allNumbers 通過 hash 離散到一個連續的數組中
        nums_map = {}
        for i, n in enumerate(sorted(allNumbers)):
            nums_map[n] = i
        root = self.buildSegTree(0, len(nums_map))
        res = 0
        # 因為這裡是看 lower + prefixSum[i-1] <= prefixSum[j] <= upper + prefixSum[i-1],每次都是從當前位置往後看所有的前綴和,所以在遍歷時,應該從最後一個前綴和往前遍歷
        for n in prefix_sum[::-1]:
            left, right = nums_map[lower + n], nums_map[upper + n]
            res += self.countOfSegTree(root, left, right)
            self.insertToSegTree(root, nums_map[n])
        return res


s = Solution()
print(s.countRangeSum([-2, 5, -1], -2, 2))

# 代碼參考了官方題解的java版本,官方版本用的是第二種移動方式

代碼中需要注意 allNumber 和 nums_map 的理解,這裡線段樹主要記錄的是在 left,right 範圍內的數值個數,因為前綴和可能比較散亂,所以對數值做了映射處理,將它映射到一個連接的數組中。

在題解中也看到了另外一種解決方法,使用的是有序數組+二分來代替的這種線段樹這種數據結構。代碼如下:

# 作者:fan-cai
# 連結:https://leetcode-cn.com/problems/count-of-range-sum/solution/python3-6xing-dai-ma-jian-ji-qian-zhui-he-er-fen-c/
# 來源:力扣(LeetCode)
# 著作權歸作者所有。商業轉載請聯繫作者獲得授權,非商業轉載請註明出處。
class Solution:
    def countRangeSum(self, nums: List[int], lower: int, upper: int) -> int:
        """按照上面的思路,一種解法就是
        計算累積和數組 sums 的,其中 sum[i] = nums[0] + nums[1] + ... + nums[i],對於某個i來說,只有那些滿足 lower <= sum[j] - sum[i] <= upper 的 j 能形成一個區間 [i, j] 滿足題意,則有:sum[i] + lower =< sum[j] <= sum[i] + upper,目標就是來找到有多少個這樣的 j滿足上述條件。從後向前遍歷累加和數組,相當於固定sum[i]後,算出有多少的sum[j]滿足左右條件。因為必須滿足0 =< i <= j,所以sum[j]的範圍一定是由sum[i]之後的元素組成的數組。對sum[j]的範圍數組排序,l是找數組中第一個大於等於給定值(左條件)的數,而 r 是找數組中最後一個小於等於給定值(右條件)的數,那麼兩者相減,就是j的個數。
        """

        import bisect
        res, pre, now = 0, [0], 0
        for n in nums:
            # now 相當於前綴和
            now += n
            # 這種解法是針對上面的第二種移動方法
            # pre記錄了從當前前綴和位置往前所有的前綴和,而且是排好序的,只需要在 pre 裡找到對應的左邊界和右邊界即可
            res += bisect.bisect_right(pre, now - lower) - \
                bisect.bisect_left(pre, now - upper)
            bisect.insort(pre, now)
        return res

三、總結

線段樹採用了二分的思想,適用在區間範圍內做查詢、更新,見到類似在區間內獲取和、最值等問題,都可以使用線段樹

個人認為線段樹問題難點在於如果構造線段樹。而如果採用連續數組的方式來存儲,要充分利用數組要存放在葉子結點這一特性

leetcode官方題解比較難理解(可能因為都是高手寫的),關鍵還是需要多看代碼,多 debug

看過關於線段樹的其實實現版本,有做懶更新與懶插入,後續有機會再詳細總結下

靈活使用位運行,2*n與2*n+1,以及快速求大於 n 的第一個 2 的 n 次冪等

四、參考資料

為了解決上面說的第二個問題,花了幾天時間,主要是連別人的題解都看不懂,參考了一些別人的解決思路,最後 debug 官方的 java 實現版本,才終於理解:

https://oi-wiki.org/ds/seg/

https://leetcode-cn.com/problems/count-of-range-sum/solution/qu-jian-he-de-ge-shu-by-leetcode-solution/

https://leetcode-cn.com/problems/count-of-range-sum/solution/xian-ren-zhi-lu-ru-he-xue-xi-ke-yi-jie-jue-ben-ti-/

https://leetcode-cn.com/problems/count-of-range-sum/solution/qian-zhui-he-xian-duan-shu-by-halfrost-2/ (其實這個人是寫得比較清楚的,但他在畫圖解釋插入的過程沒有太說清楚,看完官方代碼之後回過頭來看就很清晰了)

https://blog.csdn.net/qq_28468707/article/details/103284027 (這篇對於為什麼可以用二分,講得比較清楚)

相關焦點

  • 數據結構之php實現線段樹
    之前學習了二分搜索樹, 堆等一些數據結構. 今天我們一起學習一種新的樹形結構, 線段樹。
  • 數據結構-PHP 線段樹的實現
    1.線段樹介紹線段樹是基於區間的統計查詢,線段樹是一種 二叉搜索樹,它將一個區間劃分成一些單元區間,每個單元區間對應線段樹中的一個葉結點。
  • 什麼是線段樹?
    一、概念解析 這次來介紹一個數據結構 - 線段樹。在平時刷題或是工作中,經常會遇到這麼一個問題,「給定一個數組,求出數組某段區間的一些性質」。比如給定一個數組 [5,2,6,1,-4,0,9,2],讓你求出區間 [1,4] 上所有元素的和,在這個例子中,答案是 2 + 6 + 1 + (-4) = 5。
  • [洛穀日報第46期]線段樹的擴展之淺談zkw線段樹
    )(https://visualgo.net/zh/segmenttree)如圖,下面的黃圈是原數據,黃圈下面的紅色數字是原數組的下標上面的樹就是線段樹了,每一個節點內部都是節點下方標明的區間中所有元素的總和,上邊的黑色數字就是線段樹的下標visualgo生成的數組下標默認是從0開始的,所以線段樹下的區間和原數組有錯位,請注意區分(筆者懶得改了
  • GGTalk 中有哪些知識 - 線段樹基礎與 RMQ 問題
    所以我們的出發點是通過一種優質的數據結構,將查詢複雜度降低成 O(logn) 或者 O(1)。由於查詢次數是強需求,不是算法層面上可以優化的,所以在查詢上的效率是我們主要解決的問題。為了解決查詢問題,這一篇文章我們引入線段樹(Binary Indexed Tree)來優化 RMQ 問題的查詢操作。
  • 線段樹,看這一篇就夠了
    定義線段樹(segment tree),顧名思義, 是用來存放給定區間(segment, or interval)內對應信息的一種數據結構。與樹狀數組(binary indexed tree)相似,線段樹也用來處理數組相應的區間查詢(range query)和元素更新(update)操作。
  • 五分鐘學算法:什麼是線段樹?
    因此我們需要一個數據結構能夠幫助我們解決大部分數組的區間問題,而且時間複雜度要儘可能的低。這也就是今天的主題 - 線段樹,首先要說明一點的是,線段樹也是二叉樹,只是它的節點裡面含有區間的信息。線段線段樹每個節點表示的是一個區間,每個節點將其表示的區間一分為二,左邊分到左子樹,右邊分到右子樹,根節點表示的是整個區間(也就是整個數組),葉子節點表示的是一個 index(也就是單個元素),因為每次對半分的緣故,線段樹構建出來是平衡的,也就是說樹的高度是 O(logn),這裡的 n 表示的是數組中所有的元素,這一點對於我們後面分析複雜度很重要。線段
  • 圖解:什麼是線段樹?
    簡介 線段樹是在算法競賽中常用來維護區間信息的數據結構,同時,線段樹的理解難度較樹狀數組低(當然是在理解遞歸的前提下)。線段樹的結構與建樹 線段樹將每個長度不為1的區間劃分為左右兩個區間,並遞歸處理,通過合併左右兩個區間的信息來求得該區間的信息。對於例題來說,我們想要得到的是指定區間裡數的和,那麼,我們要求的區間信息便是該區間數的和,合併左右兩個區間的信息即是將兩個區間的值進行相加。
  • [洛穀日報第4期]淺談線段樹——Segment Tree
    一、簡介線段樹ps: _此處以詢問區間和為例。實際上線段樹可以處理很多符合結合律的操作。(比如說加法,a[1]+a[2]+a[3]+a[4]=(a[1]+a[2])+(a[3]+a[4]))線段樹之所以稱為「樹」,是因為其具有樹的結構特性。線段樹由於本身是專門用來處理區間問題的(包括RMQ、RSQ問題等。
  • 人工智慧程式設計師入門應該學哪些算法?
    三.數據結構.串排序(快排、歸併排(與逆序數有關)、堆排)簡單併查集的應用.圖的割邊和割點最小割模型、網絡流規約三.數據結構.線段樹.靜態二叉檢索樹.樹狀樹組RMQ.併查集的高級應用.KMP算法.無向圖、有向圖的最小環三.數據結構.trie圖的建立和應用.LCA和RMQ問題(LCA(最近公共祖先問題) 有離線算法(併查集+dfs) 和 在線算法雙端隊列和它的應用(維護一個單調的隊列,常常在動態規劃中起到優化狀態轉移的目的).左偏樹(可合併堆).
  • 數據結構與算法?看這篇就夠了!!!
    真正讓程式設計師有區分度,企業招聘萬年不變的重點 —— 算法與數據結構但無論半路出家還是科班出身,除學生時代搞算法競賽的同學外真正用心學習過算法與數據結構太少太少對於後期想要學習算法與數據結構卻不得不面對以下問題:於是我們推出了「數據結構與算法365天特訓營」實時視頻直播
  • 算法與數據結構?看這篇就夠了
    真正讓程式設計師有區分度,企業招聘萬年不變的重點 —— 算法與數據結構但無論半路出家還是科班出身,除學生時代搞算法競賽的同學外真正用心學習過算法與數據結構太少太少對於後期想要學習算法與數據結構卻不得不面對以下問題:於是我們推出了「數據結構與算法365天特訓營」實時視頻直播
  • 給球上色(線段樹+離散化) - HDU 1199
    使用STL離散化大大減少了代碼量且結構相當清晰。本題涉及離散化和線段樹的應用,線段樹採用數組實現,新手看起來較為吃力,可以多多琢磨實現思路。 Sample Input3 1 4 w 8 11 w 3 5 b Sample Output8 11解題思路:1、輸入區間,離散化區間2、建立線段樹,然後通過線段樹對每個節點著色3、依次計算連續區間的顏色原始碼:g++
  • PowerBI數據分析實踐02 | 結構百分比分析法
    本文為星球嘉賓"海豔"的PowerBI數據分析工作實踐系列分享之二,她深入淺出的介紹了PowerBI在數據分析中的應用,利用PowerBI發現問題分析問題
  • [洛穀日報第22期]可以代替線段樹的樹狀數組?
    引入大家學了線段樹與樹狀數組後,一定會覺得樹狀數組比線段樹好寫(背)多了,常數也小多了(分析lowbit操作,每次操作中每個節點被訪問的概率是1/2,所以常數是1/2)但是美中不足的是樹狀數組不能區間修改+區間查詢啊。事實上,樹狀數組可以做到這些,還可以查詢第k大(小)值。
  • 基於OBE的數據結構課程考核評價體系設計與實踐
    這就需要教育工作者對OBE課程教學評價進行實踐研究,設計能夠幫助學生達成畢業要求的教學方式和考核評價體系,滿足對學生基本知識、工程能力和專業素養的綜合考量。數據結構是計算機科學與技術專業重要的基礎學科,學生只有熟練掌握這一學科才能更好地學習程序的研發、設計,為其他課程的系統學習奠定基礎。
  • NLP入門之路及學習方法:從任務實踐入手!
    從任務實踐入手,做到既見樹木也見森林。筆者從2018年初開始接觸機器學習,現在是某一線網際網路公司的NLP算法工程師。從小白一步步走來,積累了一些學習和實踐過程中的經驗。現在,從個人情況、入門心得和案例分享三個方面,介紹一下NLP的入門經歷和學習方法,希望能幫助到大家。
  • 考研計算機 | 數據結構—結構算法
    數據的存儲結構實質上是它的邏輯結構在計算機存儲器中的實現,為了的反映一個數據的邏輯結構,它在存儲器中的映象包括兩方面內容,即數據元素之間的信息和數據元素之間的關係。不同數據結構有其相應的若干運算。數據的運算是在數據的邏輯結構上定義的操作算法,如檢索、插入、刪除、更新和排序等。數據的運算是數據結構的一個重要方面,討論任一種數據結構時都離不開對該結構上的數據運算及其實現算法的討論。
  • 前端學習數據結構與算法系列(一):初識數據結構與算法
    算法也是一種思想,當你去讀一些優秀框架的源碼,如果對算法和數據結構一無所知,讀起來很困難,你無法理解人家為什麼要那樣寫,那樣寫的好處是什麼,接下來就跟大家分享下作為一個前端程式設計師,如何學習數據結構與算法。
  • 快速入門數據結構和算法
    本文簡要分享算法基礎、常見的數據結構以及排序算法,給同學們帶來一堂數據結構和算法的基礎課。文末福利:阿里雲開發者訓練營來了。一  前言1  為什麼要學習算法和數據結構?2  業務開發要掌握到程度? 二  數據結構基礎1  什麼是數據結構?數據結構是數據的組織、管理和存儲格式,其使用目的是為了高效的訪問和修改數據。數據結構是算法的基石。如果把算法比喻成美麗靈動的舞者,那麼數據結構就是舞者腳下廣闊而堅實的舞臺。2  物理結構和邏輯結構的區別?