前端算法系統練習: 棧和佇列篇

語言: CN / TW / HK

這是前端算法系統練習系列的第二篇——棧和佇列篇。 我一直秉承學習這件事情一定要系統的觀念,因此每次更新都是一個完整的專題,雖然比較長,但是看下來結構會很清晰,收穫的是一塊完整的知識和方法論。希望對你能有幫助!

棧&遞迴

有效括號

給定一個只包括 '(',')','{','}','[',']' 的字串,判斷字串是否有效。

有效字串需滿足:

左括號必須用相同型別的右括號閉合。左括號必須以正確的順序閉合。注意空字串可被認為是有效字串。

示例:

輸入: "()"
輸出: true

來源: LeetCode第20題

程式碼實現

/**
* @param {string} s
* @return {boolean}
*/

var isValid = function(s) {
let stack = [];
for(let i = 0; i < s.length; i++) {
let ch = s.charAt(i);
if(ch == '(' || ch == '[' || ch == '{')
stack.push(ch);
if(!stack.length) return false;
if(ch == ')' && stack.pop() !== '(') return false;
if(ch == ']' && stack.pop() !== '[' ) return false;
if(ch == '}' && stack.pop() !== '{') return false;
}
return stack.length === 0;
};

多維陣列 flatten

將多維陣列轉化為一維陣列。

示例:

[1, [2, [3, [4, 5]]], 6] -> [1, 2, 3, 4, 5, 6]

程式碼實現

/**
* @constructor
* @param {NestedInteger[]} nestedList
* @return {Integer[]}
*/

let flatten = (nestedList) => {
let result = [];
let fn = function (target, ary) {
for (let i = 0; i < ary.length; i++) {
let item = ary[i];
if (Array.isArray(ary[i])) {
fn(target, item);
} else {
target.push(item);
}
}
}
fn(result, nestedList)
return result;

同時可採用 reduce 的方式, 一行就可以解決,非常簡潔。

let flatten = (nestedList) =>  nestedList.reduce((pre, cur) => pre.concat(Array.isArray(cur) ? flatten(cur): cur), [])

二叉樹層序遍歷

二叉樹的層序遍歷 本是下一章的內容,但是其中佇列的性質又體現得太明顯,因此就以這樣一類問題來讓大家練習佇列的相關操作。這裡會不僅僅涉及到 普通的層序遍歷 , 而且涉及到變形問題,讓大家徹底掌握。

普通的層次遍歷

給定一個二叉樹,返回其按層次遍歷的節點值。(即逐層地,從左到右訪問所有節點)。

示例:

  3
/ \
9 20
/ \
15 7

結果應輸出:

[
[3],
[9,20],
[15,7]
]

來源: LeetCode第102題

實現

/**
* @param {TreeNode} root
* @return {number[][]}
*/

var levelOrder = function(root) {
if(!root) return [];
let queue = [];
let res = [];
let level = 0;
queue.push(root);
let temp;
while(queue.length) {
res.push([]);
let size = queue.length;
// 注意一下: size -- 在層次遍歷中是一個非常重要的技巧
while(size --) {
// 出隊
let front = queue.shift();
res[level].push(front.val);
// 入隊
if(front.left) queue.push(front.left);
if(front.right) queue.push(front.right);
}
level++;
}
return res;
};

二叉樹的鋸齒形層次遍歷

給定一個二叉樹,返回其節點值的鋸齒形層次遍歷。(即先從左往右,再從右往左進行下一層遍歷,以此類推,層與層之間交替進行)。

例如:

給定二叉樹 [3,9,20,null,null,15,7], 輸出應如下:

    3
/ \
9 20
/ \
15 7

返回鋸齒形層次遍歷如下:

[
[3],
[20,9],
[15,7]
]

來源: LeetCode第103題

思路

這一題思路稍微不同,但如果把握住層次遍歷的思路,就會非常簡單。

程式碼實現

var zigzagLevelOrder = function(root) {
if(!root) return [];
let queue = [];
let res = [];
let level = 0;
queue.push(root);
let temp;
while(queue.length) {
res.push([]);
let size = queue.length;
while(size --) {
// 出隊
let front = queue.shift();
res[level].push(front.val);
if(front.left) queue.push(front.left);
if(front.right) queue.push(front.right);
}
// 僅僅增加下面一行程式碼即可
if(level % 2) res[level].reverse();
level++;
}
return res;
};

二叉樹的右檢視

給定一棵二叉樹,想象自己站在它的右側,按照從頂部到底部的順序,返回從右側所能看到的節點值。

示例:

輸入: [1,2,3,null,5,null,4]
輸出: [1, 3, 4]
解釋:

1 <---
/ \
2 3 <---
\ \
5 4 <---

來源: LeetCode第199題

思路

右檢視?如果你以 DFS 即深度優先搜尋的思路來想,你會感覺異常的痛苦。沒錯,當初我就是這樣被坑的:)

但如果用廣度優先搜尋的思想,即用層序遍歷的方式,求解這道題目也變得輕而易舉。

程式碼實現

/**
* @param {TreeNode} root
* @return {number[]}
*/

var rightSideView = function(root) {
if(!root) return [];
let queue = [];
let res = [];
queue.push(root);
while(queue.length) {
res.push(queue[0].val);
let size = queue.length;
while(size --) {
// 一個size的迴圈就是一層的遍歷,在這一層只拿最右邊的結點
let front = queue.shift();
if(front.right) queue.push(front.right);
if(front.left) queue.push(front.left);
}
}
return res;
};

無權圖 BFS 遍歷

完全平方數

給定正整數 n,找到若干個完全平方數(比如 1, 4, 9, 16, ...)使得它們的和等於 n。你需要讓組成和的完全平方數的個數最少。

示例:

輸入: n = 12
輸出: 3
解釋: 12 = 4 + 4 + 4.

來源: LeetCode第279題

思路

這一題其實最容易想到的思路是動態規劃,我們放到後面專門來拆解。實際上用佇列進行圖的建模,也是可以順利地用廣度優先遍歷的方式解決的。

看到這個圖,你可能會有點懵,我稍微解釋一下你就明白了。

在這個無權圖中,每一個點指向的都是它可能經過的上一個節點。舉例來說,對 5 而言,可能是 4 加上了 1的平方 轉換而來,也可能是1 加上了 2的平方 轉換而來,因此跟 12 都有聯絡,依次類推。

那麼我們現在要做了就是尋找到 從 n 轉換到 0 最短的連線數

舉個例子, n = 8 時,我們需要找到它的鄰居節點 47 ,此時到達 4 和到達 7 的步數都為 1, 然後分別從 4 和 7 出發,4 找到鄰居節點 30 ,達到 3 和 0 的步數都為 2,考慮到此時已經到達 0,遍歷終止,返回到達 0 的步數 2 即可。

Talk is cheap, show me your code. 我們接下來來一步步實現這個尋找的過程。

實現

接下來我們來實現第一版的程式碼。

/**
* @param {number} n
* @return {number}
*/

var numSquares = function(n) {
let queue = [];
queue.push([n, 0]);
while(queue.length) {
let [num, step] = queue.shift();
for(let i = 1; ; i ++) {
let nextNum = num - i * i;
if(nextNum < 0) break;
// 還差最後一步就到了,直接返回 step + 1
if(nextNum == 0) return step + 1;
queue.push([nextNum, step + 1]);
}
}
// 最後是不需要返回另外的值的,因為 1 也是完全平方數,所有的數都能用 1 來組合
};

這個解法從功能上來講是沒有問題的,但是其中隱藏了巨大的效能問題,你可以去LeetCode去測試一下,基本是超時。

那為什麼會出現這樣的問題?

出就出在這樣一行程式碼:

queue.push([nextNum, step + 1]);

只要是大於 0 的數,統統塞進佇列。要知道 2 - 1 = 1, 5 - 4 = 1, 9 - 8 = 1 ......這樣會重複非常多的 1 , 依次類推,也會重複非常多的 2 , 3 等等等等。

這樣大量的重複數字不僅僅消耗了更多的迴圈次數,同時也造成更加巨大的記憶體空間壓力。

因此,我們需要對已經推入佇列的數字進行標記,避免重複推入。改善程式碼如下:

var numSquares = function(n) {
let map = new Map();
let queue = [];
queue.push([n, 0]);
map.set(n, true);
while(queue.length) {
let [num, step] = queue.shift();
for(let i = 1; ; i++) {
let nextNum = num - i * i;
if(nextNum < 0) break;
if(nextNum == 0) return step + 1;
// nextNum 未被訪問過
if(!map.get(nextNum)){
queue.push([nextNum, step + 1]);
// 標記已經訪問過
map.set(nextNum, true);
}
}
}
};

單詞接龍

給定兩個單詞(beginWord 和 endWord)和一個字典,找到從 beginWord 到 endWord 的最短轉換序列的長度。轉換需遵循如下規則:

  • 每次轉換隻能改變一個字母。

  • 轉換過程中的中間單詞必須是字典中的單詞。

說明:

  1. 如果不存在這樣的轉換序列,返回 0。

  2. 所有單詞具有相同的長度。

  3. 所有單詞只由小寫字母組成。

  4. 字典中不存在重複的單詞。

  5. 你可以假設 beginWord 和 endWord 是非空的,且二者不相同。

示例:

輸入:
beginWord = "hit",
endWord = "cog",
wordList = ["hot","dot","dog","lot","log","cog"]

輸出: 5

解釋: 一個最短轉換序列是 "hit" -> "hot" -> "dot" -> "dog" -> "cog",
返回它的長度 5。

來源: LeetCode第127題

思路

這一題是一個更加典型的用圖建模的問題。如果每一個單詞都是一個節點,那麼只要和這個單詞僅有一個字母不同,那麼就是它的相鄰節點。

這裡我們可以通過 BFS 的方式來進行遍歷。實現如下:

程式碼實現

/**
* @param {string} beginWord
* @param {string} endWord
* @param {string[]} wordList
* @return {number}
*/

var ladderLength = function(beginWord, endWord, wordList) {
// 兩個單詞在圖中是否相鄰
const isSimilar = (a, b) => {
let diff = 0
for(let i = 0; i < a.length; i++) {
if(a.charAt(i) !== b.charAt(i)) diff++;
if(diff > 1) return false;
}
return true;
}
let queue = [beginWord];
let index = wordList.indexOf(beginWord);
if(index !== -1) wordList.splice(index, 1);
let res = 2;
while(queue.length) {
let size = queue.length;
while(size --) {
let front = queue.shift();
for(let i = 0; i < wordList.length; i++) {
if(!isSimilar(front, wordList[i]))continue;
// 找到了
if(wordList[i] === endWord) {
return res;
}
else {
queue.push(wordList[i]);
}
// wordList[i]已經成功推入,現在不需要了,刪除即可
// 這一步效能優化,相當關鍵,不然100%超時
wordList.splice(i, 1);
i --;
}
}
// 步數 +1
res += 1;
}
return 0;
};

實現優先佇列

所謂優先佇列,就是一種特殊的 佇列 , 其底層使用 的結構,使得每次新增或者刪除,讓隊首元素始終是優先順序最高的。關於優先順序通過什麼欄位、按照什麼樣的比較方式來設定,可以由我們自己來決定。

要實現優先佇列,首先來實現一個堆的結構。

關於堆的說明

可能你以前沒有接觸過 這種資料結構,但是其實是很簡單的一種結構,其本質就是一棵二叉樹。但是這棵二叉樹比較特殊,除了用陣列來依次儲存各個節點(節點對應的陣列下標和 層序遍歷的序號 一致)之外,它需要保證 任何一個父節點的優先順序大於子節點 ,這也是它最關鍵的性質,因為保證了根元素一定是優先順序最高的。

舉一個例子:

現在這個堆的陣列就是: [10, 7, 2, 5, 1]

因此也會產生兩個非常關鍵的操作—— siftUpsiftDown

對於 siftUp 操作,我們試想一下現在有一個正常的堆,滿足任何父元素優先順序大於子元素,這時候向這個堆的陣列末尾又添加了一個元素,那現在可能就不符合 的結構特點了。那麼現在我將新增的節點和其父節點進行比較,如果父節點優先順序小於它,則兩者交換,不斷向上比較直到根節點為止,這樣就保證了 的正確結構。而這樣的操作就是 siftUp

siftDown是與其相反方向的操作,從上到下比較,原理相同,也是為了保證堆的正確結構。

實現一個最大堆

最大堆,即堆頂元素為優先順序最高的元素。

// 以最大堆為例來實現一波
/**
* @param {number[]} nums
* @param {number} k
* @return {number[]}
*/

class MaxHeap {
constructor(arr = [], compare = null) {
this.data = arr;
this.size = arr.length;
this.compare = compare;
}
getSize() {
return this.size;
}
isEmpty() {
return this.size === 0;
}
// 增加元素
add(value) {
this.data.push(value);
this.size++;
// 增加的時候把新增的元素進行 siftUp
this._siftUp(this.getSize() - 1);
}
// 找到優先順序最高的元素
findMax() {
if (this.getSize() === 0)
return;
return this.data[0];
}
// 讓優先順序最高的元素(即隊首元素)出隊
extractMax() {
// 1.儲存隊首元素
let ret = this.findMax();
// 2.讓隊首和隊尾元素交換位置
this._swap(0, this.getSize() - 1);
// 3. 把隊尾踢出去,size--
this.data.pop();
this.size--;
// 4. 新的隊首 siftDown
this._siftDown(0);
return ret;
}

toString() {
console.log(this.data);
}
_swap(i, j) {
[this.data[i], this.data[j]] = [this.data[j], this.data[i]];
}
_parent(index) {
return Math.floor((index - 1) / 2);
}
_leftChild(index) {
return 2 * index + 1;
}
_rightChild(index) {
return 2 * index + 2;
}
_siftUp(k) {
// 上浮操作,只要子元素優先順序比父節點大,父子交換位置,一直向上直到根節點
while (k > 0 && this.compare(this.data[k], this.data[this._parent(k)])) {
this._swap(k, this._parent(k));
k = this._parent(k);
}
}
_siftDown(k) {
// 存在左孩子的時候
while (this._leftChild(k) < this.size) {
let j = this._leftChild(k);
// 存在右孩子而且右孩子比左孩子大
if (this._rightChild(k) < this.size &&
this.compare(this.data[this._rightChild(k)], this.data[j])) {
j++;
}
if (this.compare(this.data[k], this.data[j]))
return;
// 父節點比子節點小,交換位置
this._swap(k, j);
// 繼續下沉
k = j;
}
}
}

實現優先佇列

有了最大堆作鋪墊,實現優先佇列就易如反掌,廢話不多說,直接放上程式碼。

class PriorityQueue {
// max 為優先佇列的容量
constructor(max, compare) {
this.max = max;
this.compare = compare;
this.maxHeap = new MaxHeap([], compare);
}

getSize() {
return this.maxHeap.getSize();
}

isEmpty() {
return this.maxHeap.isEmpty();
}

getFront() {
return this.maxHeap.findMax();
}

enqueue(e) {
// 比當前最高的優先順序的還要高,直接不處理
if (this.getSize() === this.max) {
if (this.compare(e, this.getFront())) return;
this.dequeue();
}
return this.maxHeap.add(e);
}

dequeue() {
if (this.getSize() === 0) return null;
return this.maxHeap.extractMax();
}
}

怎麼樣,是不是非常簡單?傳說中的 優先佇列 也不過如此。

且慢,可能會有人問: 你怎麼保證這個優先佇列是正確的呢?

我們不妨來做一下測試:

let pq = new PriorityQueue(3);
pq.enqueue(1);
pq.enqueue(333);
console.log(pq.dequeue());
console.log(pq.dequeue());
pq.enqueue(3);
pq.enqueue(6);
pq.enqueue(62);
console.log(pq.dequeue());
console.log(pq.dequeue());
console.log(pq.dequeue());

結果如下:

可見,這個優先佇列的功能初步滿足了我們的預期。後面,我們將通過實際的例子來運用這種資料結構來解決問題。

優先佇列應用

前 K 個高頻元素

給定一個非空的整數陣列,返回其中出現頻率前 k 高的元素。

示例:

輸入: nums = [1,1,1,2,2,3], k = 2
輸出: [1,2]

說明:

  • 你可以假設給定的 k 總是合理的,且 1 ≤ k ≤ 陣列中不相同的元素的個數。

  • 你的演算法的時間複雜度必須優於 O(n log n) , n 是陣列的大小。

來源: LeetCode第347題

思路

首先要做的肯定是統計頻率,那之後如何來選取頻率前 K 個元素同時又保證時間複雜度小於 O(n log n)呢?

當然,這是一道典型的考察優先佇列的題,利用容量為 K 的優先佇列每次踢出不符合條件的值,那麼最後剩下的即為所求。整個時間複雜度成為 O(n log K),明顯是小於 O(n log n) 的。

既然是優先佇列,就涉及到如何來定義優先順序的問題。

倘若我們以高頻率為高優先順序,那麼隊首始終是高頻率的元素,因此每次出隊是踢出出現頻率最高的元素,假設優先佇列容量為 K,那照這麼做,剩下的是頻率最低的 K 個元素,顯然不符合題意。

因此,我們需要的是每次出隊時踢出 頻率最低的元素 ,這樣最後剩下來的就是頻率最高 K 個元素。

是不是我們為了踢出 頻率最低的元素 ,還要重新寫一個小頂堆的實現呢?

完全不需要!就像我剛才所說的,合理地定義這個優先順序的比較邏輯即可。接下來我們來具體實現一下。

程式碼實現

var topKFrequent = function(nums, k) {
let map = {};
let pq = new PriorityQueue(k, (a, b) => map[a] - map[b] < 0);
for(let i = 0; i < nums.length; i++) {
if(!map[nums[i]]) map[nums[i]] = 1;
else map[nums[i]] = map[[nums[i]]] + 1;
}
let arr = Array.from(new Set(nums));
for(let i = 0; i < arr.length; i++) {
pq.enqueue(arr[i]);
}
return pq.maxHeap.data;
};

合併 K 個排序連結串列

合併 k 個排序連結串列,返回合併後的排序連結串列。請分析和描述演算法的複雜度。

示例:

輸入:
[
1->4->5,
1->3->4,
2->6
]
輸出: 1->1->2->3->4->4->5->6

這一題我們之前在 連結串列篇 實現過,殊不知,它也可以利用優先佇列完美解決。

來源: LeetCode第23題

/**
* @param {ListNode[]} lists
* @return {ListNode}
*/

var mergeKLists = function(lists) {
let dummyHead = p = new ListNode();
// 定義優先順序的函式,重要!
let pq = new PriorityQueue(lists.length, (a, b) => a.val <= b.val);
// 將頭結點推入優先佇列
for(let i = 0; i < lists.length; i++)
if(lists[i]) pq.enqueue(lists[i]);
// 取出值最小的節點,如果 next 不為空,繼續推入佇列
while(pq.getSize()) {
let min = pq.dequeue();
p.next = min;
p = p.next;
if(min.next) pq.enqueue(min.next);
}
return dummyHead.next;
};

怎麼樣,是不是被驚豔到!原來優先佇列可以這樣來使用!

雙端佇列及應用

什麼是雙端佇列?

雙端佇列是一種特殊的佇列,首尾都可以新增或者刪除元素,是一種加強版的佇列。

JS 中的陣列就是一種典型的雙端佇列。push、pop 方法分別從 尾部 新增和刪除元素,unshift、shift 方法分別從 首部 新增和刪除元素。

滑動視窗最大值

給定一個數組 nums,有一個大小為 k 的滑動視窗從陣列的最左側移動到陣列的最右側。你只可以看到在滑動視窗內的 k 個數字。滑動視窗每次只向右移動一位。

返回滑動視窗中的最大值。

示例:

輸入: nums = [1,3,-1,-3,5,3,6,7], 和 k = 3
輸出: [3,3,5,5,6,7]
解釋:

滑動視窗的位置 最大值
--------------- -----
[1 3 -1] -3 5 3 6 7 3
1 [3 -1 -3] 5 3 6 7 3
1 3 [-1 -3 5] 3 6 7 5
1 3 -1 [-3 5 3] 6 7 5
1 3 -1 -3 [5 3 6] 7 6
1 3 -1 -3 5 [3 6 7] 7

要求: 時間複雜度應為線性。

來源: LeetCode第239題

思路

這是典型地使用雙端佇列求解的問題。

建立一個雙端佇列 window,每次 push 進來一個新的值,就將 window 中目前 前面所有比它小的值 都刪除。利用雙端佇列的特性,可以從後往前遍歷,遇到小的就刪除之,否則停止。

這樣可以保證隊首始終是最大值,因此尋找最大值的時間複雜度可以降到 O(1)。由於 window 中會有越來越多的值被淘汰,因此整體的時間複雜度是線性的。

程式碼實現

程式碼非常的簡潔,但是如果要寫出 bug free 的程式碼還是有相當的難度的,希望你能自己獨立實現一遍。

var maxSlidingWindow = function(nums, k) {
// 異常處理
if(nums.length === 0 || !k) return [];
let window = [], res = [];
for(let i = 0; i < nums.length; i++) {
// 先把滑動視窗之外的踢出
if(window[0] !== undefined && window[0] <= i - k) window.shift();
// 保證隊首是最大的
while(nums[window[window.length - 1]] <= nums[i]) window.pop();
window.push(i);
if(i >= k - 1) res.push(nums[window[0]])
}
return res;
};

棧和佇列的相互實現

棧實現佇列

使用棧實現佇列的下列操作:

push(x) -- 將一個元素放入佇列的尾部。pop() -- 從佇列首部移除元素。peek() -- 返回佇列首部的元素。empty() -- 返回佇列是否為空。

示例:

let queue = new MyQueue();

queue.push(1);
queue.push(2);
queue.peek(); // 返回 1
queue.pop(); // 返回 1
queue.empty(); // 返回 false

來源: LeetCode第232題

思路

既然棧是 先進後出 , 要想得到 先進先出 的效果,我們不妨用兩個棧。

當進行 push 操作時,push 到 stack1 ,而進行 poppeek 的操作時,我們通過 stack2

當然這其中有一個特殊情況,就是 stack2 是空,如何來進行 poppeek ? 很簡單,把 stack1 中的元素依次 pop 並推入 stack2 中,然後正常地操作 stack2 即可,如下圖所示:

這就就能保證先入先出的效果了。

程式碼實現

var MyQueue = function() {
this.stack1 = [];
this.stack2 = [];
};

MyQueue.prototype.push = function(x) {
this.stack1.push(x);
};
// 將 stack1 的元素轉移到 stack2
MyQueue.prototype.transform = function() {
while(this.stack1.length) {
this.stack2.push(this.stack1.pop());
}
}

MyQueue.prototype.pop = function() {
if(!this.stack2.length) this.transform();
return this.stack2.pop();
};

MyQueue.prototype.peek = function() {
if(!this.stack2.length) this.transform();
return this.stack2[this.stack2.length - 1];
};

MyQueue.prototype.empty = function() {
return !this.stack1.length && !this.stack2.length;
};

佇列實現棧

和上一題的效果剛好相反,用佇列 先進先出 的方式來實現 先進後出 的效果。

思路

以上面的佇列為例,push 操作好說,直接從在佇列末尾推入。但 pop 和 peek 呢?

回到我們的目標,我們的目標是拿到隊尾的值,也就是 3 。這就好辦了,我們讓前面的元素統統出隊,只留隊尾元素即可,剩下的元素讓另外一個佇列儲存。

來源: LeetCode第225題

程式碼實現

實現過程中,值得注意的一點是, queue1 始終儲存前面的元素,queue2 始終儲存隊尾元素(即棧頂元素 )

但是當 push 的時候有一個陷阱,就是當 queue2 已經有元素的時候,不能將新值 push 到 queue1 ,因為此時的 棧頂元素 應該更新。此時對於新的值來說,應先 push 到 queue2, 然後將 舊的棧頂 從queue2出隊,推入 queue1 ,這樣就實現了 更新棧頂 的操作。

var MyStack = function() {
this.queue1 = [];
this.queue2 = [];
};
MyStack.prototype.push = function(x) {
if(!this.queue2.length) this.queue1.push(x);
else {
// queue2 已經有值
this.queue2.push(x);
// 舊的棧頂移到 queue1 中
this.queue1.push(this.queue2.shift());
}

};
MyStack.prototype.transform = function() {
while(this.queue1.length !== 1) {
this.queue2.push(this.queue1.shift())
}
// queue2 儲存了前面的元素
// 讓 queue1 和 queue2 交換
// 現在queue1 包含前面的元素,queue2 裡面就只包含隊尾的元素
let tmp = this.queue1;
this.queue1 = this.queue2;
this.queue2 = tmp;
}
MyStack.prototype.pop = function() {
if(!this.queue2.length) this.transform();
return this.queue2.shift();
};
MyStack.prototype.top = function() {
if(!this.queue2.length) this.transform();
return this.queue2[0];
};
MyStack.prototype.empty = function() {
return !this.queue1.length && !this.queue2.length;
};

-   E N D   -

3 6 0 W 3 C E C M A T C 3 9 L e a d e r 注和加