目的: 引導學員挑戰 LeetCode 並掌握位元處理和記憶體管理
作答表單
測驗 1
LeetCode 1409. Queries on a Permutation With Key 題意為,給定一個待查陣列 queries
,陣列中的元素為 1 到 m 之間的正整數。 請你根據以下規則處理所有待查項 (從 到 ):
- 起初排列
- 對於目前 ,請你找出待查項 在排列 P 中的位置(索引自 0 開始),然後將其從原位置移動到排列 P 的起始位置 (即索引為 0 處)。注意 在 P 中的位置就是 的查詢結果。
請你以陣列形式返回待查陣列 queries
的查詢結果。
範例 1
- 輸入
,
- 輸出
- 解讀:待查陣列
queries
處理如下:
- 對於
3
在 P 中的位置是 2
,接著我們把 3
移動到 P 的起始位置,得到
- 對於
1
在 P 中的位置是 1
,接著我們把 1
移動到 P 的起始位置,得到
- 對於
2
在 P 中的位置是 2
,接著我們把 2
移動到 P 的起始位置,得到
- 對於
1
在 P 中的位置是 1
,接著我們把 1
移動到 P 的起始位置,得到
- 因此,返回的結果陣列為
範例 2
- 輸入
,
- 輸出
- 解讀:待查陣列
queries
處理如下:
- 對於
4
在 P 中的位置是 3
,接著我們把 4
移動到 P 的起始位置,得到
- 對於
1
在 P 中的位置是 1
,接著我們把 1
移動到 P 的起始位置,得到
- 對於
2
在 P 中的位置是 2
,接著我們把 2
移動到 P 的起始位置,得到
- 對於
2
在 P 中的位置是 0
,接著我們把 2
移動到 P 的起始位置,得到
- 因此,返回的結果陣列為
範例 3
- 輸入
,
- 輸出
- 解讀:待查陣列
queries
處理如下:
- 對於
7
在 P 中的位置是 6
,接著我們把 7
移動到 P 的起始位置,得到
- 對於
5
在 P 中的位置是 5
,接著我們把 5
移動到 P 的起始位置,得到
- 對於
5
在 P 中的位置是 0
,接著我們把 5
移動到 P 的起始位置,得到
- 對於
8
在 P 中的位置是 7
,接著我們把 8
移動到 P 的起始位置,得到
- 對於
3
在 P 中的位置是 5
,接著我們把 3
移動到 P 的起始位置,得到
- 因此,返回的結果陣列為
提示:
思路:我們可維護一個陣列 pos[]
,記錄目前所有數字的位置,初始時數字的位置對應其本身的數值減 1
(數字自 1
開始,而索引自 0
起),也就是:
接著我們循序走訪 queries
陣列中的每個數字,利用 pos 陣列查看目前數字所在的位置,並將該位置計入預計回傳的結果。由於數字會從目前位置移動到陣列的首位,因此,目前數字的位置變為 0
,其他小於目前數字的位置都要加 1
,位置大於目前數字的位置保持不變。重複此步驟,直到處理完所有 queries
中的數字。
為了縮減時間複雜度,我們可引入 Fenwick tree:
Binary indexed tree: Fenwick tree 是個處理前綴時很有效率的資料結構,也稱為 Binary indexed tree,或簡稱 BIT。以下簡稱它為 BIT。對於一個長度為 n 的陣列,BIT 可以在 的時間初始化,在 時間詢問一個前綴的訊息 (例如前綴和),以及在 的時間修改其中一個值。雖然複雜度和線段樹相同,但 BIT 的時間常數比線段樹小得多,空間也比較小 (只需要多開一個大小恰好為 n 的陣列即可),程式碼也非常精簡 。一般來說,如果問題的性質可以使用 BIT,效率會和使用線段樹有明顯的差別。
lowbit
: 二進位最後一個 1 的位置所代表的數值
範例:
- 13 = 11012
lowbit(13)
= 00012 = 1
- 12 = 11002
lowbit(12)
= 01002 = 4

節點存 的和。
父節點為
第一列為原陣列 (即 ,第二到第四列依次填入數字
方便起見,此處的陣列的索引為自 1
開始
建構 BIT:
- prefixSum(13) = prefixSum(000011012)
= BIT[13] + BIT[12] + BIT[8]
= BIT[000011012] + BIT[000011002] + BIT[000010002]

樹的示意圖

BIT 的 binary 並不是指二元樹,而是指「二進位」
更新操作:

原有位於索引 5 的數值 5
增加 2
而成為 7
BIT 時間複雜度
考慮以下程式碼為 1409. Queries on a Permutation With Key 的可能解法:
請補完上述程式碼,使其得以符合 LeetCode 題目的行為。
參考資料:
作答區
PPP = ?
QQQ = ?
(a)
i
(b)
0
(c)
queries_size
(d)
queries_size - 1
(e)
queries_size - i
延伸問題:
- 解釋上述程式碼運作原理,並分析時間複雜度
- Binary Indexed Tree 有哪些應用?請舉例並分析
- 研讀 Fenwick Treeの定数倍高速化,探究上述程式碼改進的機會
測驗 2
CS:APP 第 9 章提到 garbage collector (以下簡稱 GC),其中 Mark and Sweep 是經典的演算法,後者分為 mark 和 Sweep 兩個階段:
- Mark 階段:
從 root set(stack & globals list)遞迴地走訪可到達的記憶體區段,並將之標記
- Sweep 階段:
走訪所有記憶體區段(ptr_map 中所有節點),並將沒被標記到的從 heap 中釋放
示意如下:

實作 Mark
:
- 建立存取清單
todo
將所有 root 中的物件加入。
- 當
todo
中有物件時,執行以下3~4。
- 從
todo
中選任一物件 v
,將其除外於 todo
。
- 如果
v
未被標記,標記 v
,將所有 v
包含的物件放到 todo
中。
實作 Sweep
:
- 物件
p
從 heap
的最底層物件開始。
- 當
p
還不是 top
的時候,執行以下3~5。
- 如果
p
已被標記,則將其改為未標記。
- 反之,將
p
的記憶體區塊加到 free list
中,準備清除。
- 將
p
改為在 heap 中上一層的物件。 (using p <- p + sizeof(p)
)
問題點 1
- 需要清除時,通常表示沒有記憶體空間。
- 但是, Mark 階段需要記憶體空間去創造訪問清單 。
- 存取清單無法預測大小,因此無法預先保留。
解決方法
- 應用
pointer reversal
,用 depth first search
取代存取清單。
問題點 2
解決方法
- 使用一個暫存器記憶其
parent
位置,就可不用 pointer reversal
也能執行 depth firsth search
。
上述的 mark and sweep 演算法有個缺點。每次在執行 mark
時,都需要將程式的執行予以暫停。
- 標記時,需要讓所有 object 維持不變,才能保持不出錯。
舉例: 以下程式具備物件 obj0, obj1, obj2, obj3
時間 t0:執行 mark
將 reachable
物件標記。

時間 t1:因為沒有暫停程式的執行,出現 obj4 指向 obj3

時間 t2: 執行 sweep
,刪除物件 obj3, obj4 產生錯誤。

因此,為了讓 mark and sweep 過程可不暫停程式的運作, 延伸出 Tri-colored 演算法,描述如下:
- 使用 Breadth-first 的策略。
- 將物件用三種顏色來標記:
- 白色:未標記,在 sweep 的階段被清掃
- 灰色:已標記,目前 Breadth-first 的進行前緣。
- 黑色:已標記。
- 將新增物件,標記為灰色。
以下是改寫自 Baby's First Garbage Collector 的虛擬機器和 GC 實作:
參考的程式執行輸出如下: (預期「不會」觸發 assertion)
請補完程式碼,使 GC 符合預期行為。
作答區
AAA = ?
(a)
++vm->object_num
(b)
--vm->object_num
BBB = ?
(a)
++vm->object_num
(b)
--vm->object_num
CCC = ?
(a)
2
(b)
3
(c)
4
(d)
5
(e)
6
(f)
7
(g)
8
DDD = ?
(b)
3
(c)
4
(d)
5
(e)
6
(f)
7
(g)
8
延伸問題:
- 解釋上述程式碼運作原理,並且指出其不足和實作錯誤之處
- 針對 Tri-colored 演算法,改進上述程式碼,使得符合演算法設計
測驗 3
Gap Buffer 是種動態陣列的實作方式,廣泛用於文字編輯器一類的程式。其手法是將「空白」(gap, 指字串沒有填滿緩衝區的部分) 移動到緩衝區中間,如下圖:

當我們執行插入或刪除動作時,緩衝區中的 gap 會隨之移動。例如,下面一句話中,初始情況下空白在緩衝區的最後:
這裡我們使用中括號 (即 [ ]
) 表示空白
當我們想要在 smple
中插入 a
字元時,編輯器首先把空白移動到 s
後面:
然後在 s
後面插入 a
:
若我們想在 txt
中插入一個 e
,則需要同樣的步驟:
這種方法所基於的假設是,編輯文本時通常為連續接受輸入,其他時間則較少更動。
部分 Emacs 變種使用 Gap Buffer,包括古老的 Emacs on TECO、現代的 GNU/Emacs 及早期 Gosling Emacs (Java 之父 James Gosling 在 1981 年用 C 語言撰寫的 Emacs 編輯器,後來 Richard Stallman 取用部分程式碼打造出 GNU Emacs),和 Scintilla (包括 Code::Blocks 許多整合開發環境裡頭使用的程式碼編輯器元件) 也使用 Gap Buffer。某些 Emacs 版本改用 Linked Line。
一個實作案例:假設文字編輯器的編輯區已有 Hi, world!
字串,然後游標 (cursor) 在 ,
前面,我們用下方表示法:
gap buffer 會先建立下方的陣列,在此我們假設 gap 的長度是 4 且僅有一個 (事實上可能有不少這種 gap),\xx
表示 ASCII 編碼為 16 進位 xx 的字元:
當使用者刪去一個字元後,變成這樣:
使用者輸入 ello
,變成這樣:
然後使用者右移游標,變成這樣:
需要注意的是,當遇到緩衝區填滿的情況,需要重新配置記憶體再複製資料內容,所以 gap buffer 對於大檔案的處理會有效率疑慮。
走訪上述的編輯過程:
考慮以下 Gap Buffer 實作程式碼:
#include <stdlib.h>
typedef struct {
char *buf;
size_t size;
size_t lb, rb;
} gapbuf_buffer_t;
typedef enum {
GAPBUF_SUCCESS = 0,
GAPBUF_ENOMEM,
GAPBUF_ERANGE,
GAPBUF_EARG,
GAPBUF_EMAX
} gapbuf_error_t;
gapbuf_error_t gapbuf_insert(gapbuf_buffer_t *gb,
const char *buf,
const size_t n);
gapbuf_error_t gapbuf_delete(gapbuf_buffer_t *gap, size_t n);
#include <limits.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
static size_t _gapbuf_calc_buf_capacity(const size_t n)
{
size_t v = n - 1;
v |= v >> 1;
v |= v >> 2;
v |= v >> 4;
v |= v >> 8;
v |= v >> 16;
#if (__WORDSIZE == 64)
v |= v >> 32;
#endif
v++;
return v;
}
static int _gapbuf_provision_buf_capacity(gapbuf_buffer_t *gap, const size_t n)
{
if (gap->lb + n >= gap->rb) {
size_t new_siz = _gapbuf_calc_buf_capacity(gap->size + n);
char *new_buf;
if (!(new_buf = malloc(new_siz)))
return -1;
size_t new_rb = K333 - gap->size;
size_t rlen = gap->size - K444;
if (gap->buf) {
memcpy(new_buf, gap->buf, gap->lb);
memcpy(new_buf + new_rb, gap->buf + gap->rb, rlen);
}
char *tmp = gap->buf;
gap->buf = new_buf;
gap->rb = new_rb;
gap->size = new_siz;
free(tmp);
}
return 0;
}
gapbuf_error_t gapbuf_alloc(gapbuf_buffer_t **gb,
const char *content,
const size_t len)
{
if (!(*gb = malloc(sizeof(gapbuf_buffer_t))))
return GAPBUF_ENOMEM;
(*gb)->size = 0;
(*gb)->lb = 0, (*gb)->rb = 0;
(*gb)->buf = NULL;
K111;
return GAPBUF_SUCCESS;
}
gapbuf_error_t gapbuf_free(gapbuf_buffer_t *gb)
{
if (gb) {
free(gb->buf);
free(gb);
}
return GAPBUF_SUCCESS;
}
size_t gapbuf_read(const gapbuf_buffer_t *gb, char *buf, const size_t bufsiz)
{
if (!(gb && buf) || (buf && !bufsiz))
return GAPBUF_EARG;
size_t lsiz = gb->lb;
size_t n = (bufsiz < lsiz) ? bufsiz : lsiz;
memcpy(buf, gb->buf, n);
size_t rsiz = bufsiz > 0 ? K222 : 0;
n = (n < rsiz) ? n : rsiz;
memcpy(buf + gb->lb, gb->buf + gb->rb, n);
size_t total_len = gb->lb + rsiz;
size_t term_index = total_len < bufsiz ? total_len : bufsiz;
buf[term_index] = '\0';
return term_index;
}
gapbuf_error_t gapbuf_insert(gapbuf_buffer_t *gb,
const char *buf,
const size_t n)
{
if (!(gb && buf))
return GAPBUF_EARG;
if (_gapbuf_provision_buf_capacity(gb, n) != 0)
return GAPBUF_ENOMEM;
memcpy(gb->buf + gb->lb, buf, n);
gb->lb += n;
return GAPBUF_SUCCESS;
}
gapbuf_error_t gapbuf_delete(gapbuf_buffer_t *gb, size_t n)
{
if (n > gb->lb)
return GAPBUF_ERANGE;
gb->lb -= n;
return GAPBUF_SUCCESS;
}
gapbuf_error_t gapbuf_fwd(gapbuf_buffer_t *gb, size_t n)
{
if (n + gb->rb > gb->size)
return GAPBUF_ERANGE;
memmove(gb->buf + gb->lb, gb->buf + gb->rb, n);
gb->lb += n;
gb->rb += n;
return GAPBUF_SUCCESS;
}
gapbuf_error_t gapbuf_rwd(gapbuf_buffer_t *gb, size_t n)
{
if (n > gb->lb)
return GAPBUF_ERANGE;
memmove(gb->buf + gb->rb - n, gb->buf + gb->lb - n, n);
gb->lb -= n;
gb->rb -= n;
return GAPBUF_SUCCESS;
}
#define my_assert(test, message) \
do { \
if (!(test)) \
return message; \
} while (0)
#define my_run_test(test) \
do { \
char *message = test(); \
tests_run++; \
if (message) \
return message; \
} while (0)
int tests_run = 0;
static char *test_gapbuf_alloc_happy_path()
{
printf("%s... ", __func__);
char *expected = "0123456789";
gapbuf_buffer_t *gap;
my_assert(gapbuf_alloc(&gap, expected, strlen(expected)) == GAPBUF_SUCCESS,
"Could not allocate buffer");
my_assert(gap->size == 16, "Unexpected allocation size");
char data[32];
size_t n = gapbuf_read(gap, data, 32);
data[n] = '\0';
gapbuf_free(gap);
my_assert(strncmp(expected, data, n) == 0,
"Initialization string is not consistent");
printf("OK\n");
return 0;
}
static char *test_gapbuf_alloc_zero_length()
{
printf("%s... ", __func__);
gapbuf_buffer_t *gap;
my_assert(gapbuf_alloc(&gap, "", 0) == GAPBUF_SUCCESS,
"Could not allocate empty buffer");
my_assert(gap, "Empty buffer allocation failed");
my_assert(gap->size == 0, "Unexpected allocation size");
my_assert(gapbuf_insert(gap, "asdf", 4) == GAPBUF_SUCCESS,
"Could not insert into empty buffer");
char data[8];
size_t n = gapbuf_read(gap, data, 8);
my_assert(strncmp("asdf", data, n) == 0, "String inconsistency");
gapbuf_free(gap);
printf("OK\n");
return 0;
}
static char *test_gapbuf_read_into_insufficient_buffer()
{
printf("%s... ", __func__);
gapbuf_buffer_t *gap;
my_assert(gapbuf_alloc(&gap, "0123456789", 10) == GAPBUF_SUCCESS,
"Could not allocate buffer");
char too_short[4];
size_t n = gapbuf_read(gap, too_short, 4);
my_assert(strncmp("0123456789", too_short, n) == 0,
"String inconsistency for short buffer");
printf("OK\n");
return 0;
}
static char *test_suite()
{
my_run_test(test_gapbuf_alloc_happy_path);
my_run_test(test_gapbuf_alloc_zero_length);
my_run_test(test_gapbuf_read_into_insufficient_buffer);
return 0;
}
int main()
{
char *result = test_suite();
if (result != 0) {
printf("%s\n", result);
} else {
printf("ALL TESTS PASSED\n");
}
printf("Tests run: %d\n", tests_run);
return result != 0;
}
參考的執行輸出如下:
請參照程式碼註解,補完程式碼。
作答區
K111 = ?
(a)
空白即可
(b)
if (content) return gapbuf_insert(*gb, content, len)
(c)
if (len > 0) return gapbuf_insert(*gb, content, len)
(d)
if (content && len) return gapbuf_insert(*gb, content, len)
K222 = ?
(a)
gb->size - gb->rb - 1
(b)
gb->size - gb->rb
(c)
gb->size - gb->lb
(d)
gb->size - gb->lb - 1
(e)
gb->rb
(f)
gb->lb
K333 = ?
(a)
gap->lb
(b)
gap->lb + gap->rb
(c)
gap->lb + new_siz
(d)
gap->rb + new_siz
K444 = ?
(a)
gap->lb
(b)
gap->rb
(c)
(gap->rb - 1)
(d)
(gap->rb + 1)
(e)
(gap->lb - 1)
(f)
(gap->lb + 1)
延伸問題:
- 解釋上述程式碼運作原理並指出實作的缺失;
- 透過 gcc builtins 和硬體指令去改進
_gapbuf_calc_buf_capacity
的效能;
- 以上述程式碼為基礎,開發簡易的文字編輯器;
- 學習 Mazu-Editor 實作手法,探究其原理;