1. 程式人生 > >淺析Lua中table的遍歷和刪除(轉)

淺析Lua中table的遍歷和刪除(轉)

當我在工作中使用lua進行開發時,發現在lua中有4種方式遍歷一個table,當然,從本質上來說其實都一樣,只是形式不同,這四種方式分別是:

  1. forkey, value in pairs(tbtest) do  
  2. XXX  
  3. end
  4. forkey, value in ipairs(tbtest) do  
  5. XXX  
  6. end
  7. for i=1, #(tbtest) do  
  8.     XXX  
  9. end
  10. for i=1, table.maxn(tbtest) do  
  11.     XXX  
  12. end

前兩種是泛型遍歷,後兩種是數值型遍歷。當然你還會說lua的table遍歷還有很多種方法啊,沒錯,不過最常見的這些遍歷確實有必要弄清楚。

這四種方式各有特點,由於在工作中我幾乎每天都會使用遍歷table的方法,一開始也非常困惑這些方式的不同,一段時間後才漸漸明白,這裡我也是把自己的一點經驗告訴大家,對跟我一樣的lua初學者也許有些幫助(至少當初我在寫的時候在網上就找了很久,不知道是因為大牛們都認為這些很簡單,不需要說,還是因為我笨,連這都要問)。

首先要明確一點,就是lua中table並非像是C/C++中的陣列一樣是順序儲存的,準確來說lua中的table更加像是C++中的map,通過Key對應儲存Value,但是並非順序來儲存key-value對,而是使用了hash的方式,這樣能夠更加快速的訪問key對應的value,我們也知道hash表的遍歷需要使用所謂的迭代器來進行,同樣,lua也有自己的迭代器,就是上面4種遍歷方式中的pairs和ipairs遍歷。但是lua同時提供了按照key來遍歷的方式(另外兩種,實質上是一種),正式因為它提供了這種按key的遍歷,才造成了我一開始的困惑,我一度認為lua中關於table的遍歷是按照我table定義key的順序來的。

下面依次來講講四種遍歷方式,首先來看for k,v in pairs(tbtest) do這種方式:

先看效果:

  1. tbtest = {  
  2.     [1] = 1,  
  3.     [2] = 2,  
  4.     [3] = 3,  
  5.     [4] = 4,  
  6. forkey, value in pairs(tbtest) do  
  7.     print(value)  
  8. end

我認為輸出應該是1,2,3,4,實際上的輸出是1,2,4,3。我因為這個造成了一個bug,這是後話。

也就是說for k,v in pairs(tbtest) do 這樣的遍歷順序並非是tbtest中table的排列順序,而是根據tbtest中key的hash值排列的順序來遍歷的。

當然,同時lua也提供了按照key的大小順序來遍歷的,注意,是大小順序,仍然不是key定義的順序,這種遍歷方式就是for k,v in ipairs(tbtest) do。

for k,v in ipairs(tbtest) do 這樣的迴圈必須要求tbtest中的key為順序的,而且必須是從1開始,ipairs只會從1開始按連續的key順序遍歷到key不連續為止。

  1. tbtest = {  
  2. [1] = 1,  
  3. [2] = 2,  
  4. [3] = 3,  
  5. [5] = 5,  
  6. for k,v in ipairs(tbtest) do  
  7. print(v)  
  8. end

只會列印1,2,3。而5則不會顯示。

  1. local tbtest = {  
  2. [2] = 2,  
  3. [3] = 3,  
  4. [5] = 5,  
  5. for k,v in ipairs(tbtest) do  
  6. print(v)  
  7. end

這樣就一個都不會列印。

第三種遍歷方式有一種神奇的符號'#',這個符號的作用是是獲取table的長度,比如:

  1. tbtest = {  
  2. [1] = 1,  
  3. [2] = 2,  
  4. [3] = 3,  
  5. }  
  6. print(#(tbtest)) 

列印的就是3

  1. tbtest = {  
  2. [1] = 1,  
  3. [2] = 2,  
  4. [6] = 6,  
  5. }  
  6. print(#(tbtest)) 

這樣列印的就是2,而且和table內的定義順序沒有關係,無論你是否先定義的key為6的值,‘#’都會查詢key為1的值開始。

如果table的定義是這樣的:

  1. tbtest = {  
  2. ["a"] = 1,  
  3. [2] = 2,  
  4. [3] = 3,  
  5. print(#(tbtest)) 

那麼列印的就是0了。因為‘#’沒有找到key為1的值。同樣:

  1. tbtest = {  
  2. [“a”] = 1,  
  3. [“b”] = 2,  
  4. [“c”] = 3,  
  5. }  
  6. print(#(tbtest)) 

列印的也是0

所以,for i=1, #(tbtest) do這種遍歷,只能遍歷當tbtest中存在key為1的value時才會出現結果,而且是按照key從1開始依次遞增1的順序來遍歷,找到一個遞增不是1的時候就結束不再遍歷,無論後面是否仍然是順序的key,比如:

table.maxn獲取的只針對整數的key,字串的key是沒辦法獲取到的,比如:

  1. tbtest = {  
  2. [1] = 1,  
  3. [2] = 2,  
  4. [3] = 3,  
  5. }  
  6. print(table.maxn(tbtest)) 
  7. tbtest = {  
  8. [6] = 6,  
  9. [1] = 1,  
  10. [2] = 2,  
  11. }  
  12. print(table.maxn(tbtest)) 

這樣列印的就是3和6,而且和table內的定義順序沒有關係,無論你是否先定義的key為6的值,table.maxn都會獲取整數型key中的最大值。

如果table的定義是這樣的:

  1. tbtest = {  
  2. ["a"] = 1,  
  3. [2] = 2,  
  4. [3] = 3,  
  5. }  
  6. print(table.maxn(tbtest)) 

那麼列印的就是3了。如果table是:

  1. tbtest = {  
  2. [“a”] = 1,  
  3. [“b”] = 2,  
  4. [“c”] = 3,  
  5. }  
  6. print(table.maxn(tbtest))  
  7. print(#(tbtest)) 

那麼列印的就全部是0了。

換句話說,事實上因為lua中table的構造表示式非常靈活,在同一個table中,你可以隨意定義各種你想要的內容,比如:

  1. tbtest = {  
  2. [1] = 1,  
  3. [2] = 2,  
  4. [3] = 3,  
  5. ["a"] = 4,  
  6. ["b"] = 5,  

同時由於這個靈活性,你也沒有辦法獲取整個table的長度,其實在coding的過程中,你會發現,你真正想要獲取整個table長度的地方几乎沒有,你總能採取一種非常巧妙的定義方式,把這種需要獲取整個table長度的操作避免掉,比如:

  1. tbtest = {  
  2. tbaaa = {  
  3. [1] = 1,  
  4. [2] = 2,  
  5. [3] = 3,  
  6. },  
  7. ["a"] = 4,  
  8. ["b"] = 5,  

你可能會驚訝,上面這種table該如何遍歷呢?

  1. for k, v in pairs(tbtest) do  
  2. print(k, v)  
  3. end

輸出是:a 4 b 5 tbaaa table:XXXXX。

由此你可以看到,其實在table中定義一個table,這個table的名字就是key,對應的內容其實是table的地址。

當然,如果你用

  1. for k, v in ipairs(tbtest) do  
  2. print(k,v)  
  3. end

來遍歷的話,就什麼都不會列印,因為沒有key為1的值。但當你增加一個key為1的值時,ipairs只會列印那一個值,現在你明白ipairs是如何工作的吧。

既然這裡談到了遍歷,就說一下目前看到的幾種針對table的遍歷方式:

for i=1, #tbtest do --這種方式無法遍歷所有的元素,因為'#'只會獲取tbtest中從key為1開始的key連續的那幾個元素,如果沒有key為1,那麼這個迴圈將無法進入

for i=1, table.maxn(tbtest) do --這種方式同樣無法遍歷所有的元素,因為table.maxn只會獲取key為整數中最大的那個數,遍歷的元素其實是查詢tbtest[1]~tbtest[整數key中最大值],所以,對於string做key的元素不會去查詢,而且這麼查詢的效率低下,因為如果你整數key中定義的最大的key是10000,然而10000以下的key沒有幾個,那麼這麼遍歷會浪費很多時間,因為會從1開始直到10000每一個元素都會查詢一遍,實際上大多數元素都是不存在的,比如:

  1. tbtest = {  
  2. [1] = 1,  
  3. [10000] = 2,  
  4. }  
  5. localcount = 0  
  6. for i=1, table.maxn(tbtest) do  
  7. count = count + 1  
  8. print(tbtest[i])  
  9. end
  10. print(count

你會看到列印結果是多麼的坑爹,只有1和10000是有意義的,其他的全是nil,而且count是10000。耗時非常久。一般我不這麼遍歷。但是有一種情況下又必須這麼遍歷,這個在我的工作中還真的遇到了,這是後話,等講完了再談。

  1. for k, v in pairs(tbtest) do 

這個是唯一一種可以保證遍歷tbtest中每一個元素的方式,別高興的太早,這種遍歷也有它自身的缺點,就是遍歷的順序不是按照tbtest定義的順序來遍歷的,這個前面講到過,當然,對於不需要順序遍歷的用法,這個是唯一可靠的遍歷方式。

  1. for k, v in ipairs(tbtest) do 

這個只會遍歷tbtest中key為整數,而且必須從1開始的那些連續元素,如果沒有1開始的key,那麼這個遍歷是無效的,我個人認為這種遍歷方式完全可以被改造table和for i=1, #(tbtest) do的方式來代替,因為ipairs的效果和'#'的效果,在遍歷的時候是類似的,都是按照key的遞增1順序來遍歷。

好,再來談談為什麼我需要使用table.maxn這種非常浪費的方式來遍歷,在工作中, 我遇到一個問題,就是需要把當前的周序,轉換成對應的獎勵,簡單來說,就是從一個活動開始算起,每週的獎勵都不是固定的,比如1~4周給一種獎勵,5~8周給另一種獎勵,或者是一種排名獎勵,1~8名給一種獎勵,9~16名給另一種獎勵,這種情況下,我根據長久的C語言的習慣,會把table定義成這個樣子:

  1. tbtestAward = {  
  2. [8] = 1,  
  3. [16] = 3,  

這個代表,1~8給獎勵1,9~16給獎勵3。這樣定義的好處是獎勵我只需要寫一次(這裡的獎勵用數字做了簡化,實際上獎勵也是一個大的table,裡面還有非常複雜的結構)。然後我就遇到一個問題,即我需要根據周序數,或者是排名序數來確定給哪一種獎勵,比如當前周序數是5,那麼我應該給我定義好的key為8的那一檔獎勵,或者當前周序數是15,那麼我應該給獎勵3。由此讀者看出,其實我定義的key是一個分界,小於這個key而大於上一個key,那麼就給這個key的獎勵,這就是我判斷的條件。邏輯上沒有問題,但是lua的遍歷方式卻把我狠狠地坑了一把。讀者可以自己想一想我上面介紹的4種遍歷方式,該用哪一種來實現我的這種需求呢?這個函式的大致框架如下:

  1. function GetAward(nSeq)  
  2. for 遍歷整個獎勵表 do  
  3. if 滿足key的條件 then
  4. return 返回對應獎勵的key
  5. end
  6. end
  7. return nil  
  8. end

我也不賣關子了,分別來說一說吧,首先因為我的key不是連續的,而且沒有key為1的值,所以ipairs和'#'遍歷是沒用的。這種情況下理想的遍歷貌似是pairs,因為它會遍歷我的每一個元素,但是讀者不要忘記了,pairs遍歷並非是按照我定義的順序來遍歷,如果我真的使用的條件是:序數nSeq小於這個key而大於上一個key,那麼就返回這個key。那麼我無法保證程式執行的正確性,因為key的順序有可能是亂的,也就是有可能先遍歷到的是key為16的值,然後才是key為8的值。

這麼看來我只剩下table.maxn這麼一種方式了,於是我寫下了這種程式碼:

  1. for i=1, table.maxn(tbtestAward) do  
  2. if tbtestAward[i] ~= nil then
  3. if nSeq <= i then
  4. return i  
  5. end
  6. end
  7. end

這麼寫效率確實低下,因為實際上還是遍歷了從key為1開始直到key為table.maxn中間的每一個值,不過能夠滿足我上面的要求。當時我是這麼實現的,因為這個獎勵表會不斷的發生變化,這樣我每次修改只需要修改這個獎勵表就能夠滿足要求了,後來我想了想,覺得其實我如果自己再定義一個序數轉換成對應的獎勵數種類的表就可以避免這種坑爹的操作了,不過如果獎勵發生修改,我需要統一排查的地方就不止這個獎勵表了,權衡再三,我還是沒有改,就這麼寫了。沒辦法,不斷變化的需求已經把我磨練的忘記了程式的最高理想。我甚至願意犧牲演算法的效率而去追求改動的穩定性。在此哀悼程式設計師的無奈。我這種時間換空間的做法確實不知道好不好。

後來我在《Programming In Lua》中看到了一個神奇的迭代器,使用它就可以達到我想要的這種遍歷方式,而且不需要去遍歷那些不存在的key。它的方法是把你所需要遍歷的table裡的key按照遍歷順序放到另一個臨時的table中去,這樣只需要遍歷這個臨時的table按順序取出原table中的key就可以了。如下:

首先定義一個迭代器:

  1. function pairsByKeys(t)  
  2.     local a = {}  
  3.     for n in pairs(t) do  
  4.         a[#a+1] = n  
  5.     end
  6.     table.sort(a)  
  7.     local i = 0  
  8.     returnfunction()  
  9.         i = i + 1  
  10.         return a[i], t[a[i]]  
  11.     end
  12. end

然後在遍歷的時候使用這個迭代器就可以了,table同上,遍歷如下:

  1. forkey, value in pairsByKeys(tbtestAward) do  
  2. if nSeq <= keythen
  3. returnkey
  4. end
  5. end

並且後來我發現有了這個迭代器,我根本不需要先做一步獲取是哪一檔次的獎勵的操作,直接使用這個迭代器進行發獎就可以了。大師就是大師,我怎麼就沒想到呢!

還有些話我還沒有說,比如上面數值型遍歷也並非是像看起來那樣進行遍歷的,比如下面的遍歷:

  1. tbtest = {  
  2.     [1] = 1,  
  3.     [2] = 2,  
  4.     [3] = 3,  
  5.     [5] = 5,  
  6. for i=1, #(tbtest) do  
  7.     print(tbtest[i])  
  8. end

列印的順序是:1,2,3。不會列印5,因為5已經不在table的陣列資料塊中了,我估計是被放到了hash資料塊中,但是當我修改其中的一些key時,比如:

  1. tbtest = {  
  2.     [1] = 1,  
  3.     [2] = 2,  
  4.     [4] = 4,  
  5.     [5] = 5,  
  6. for i=1, #(tbtest) do  
  7.     print(tbtest[i])  
  8. end

列印的內容卻是:1,2,nil,4,5。這個地方又遍歷到了中間沒有的key值,並且還能繼續遍歷下去。我最近正在看lua原始碼中table的實現部分,已經明白了是怎麼回事,不過我想等我能夠更加清晰的闡述lua中table的實現過程了再向大家介紹。用我師傅的話說就是不要使用一些未定義的行為方法,避免在工作中出錯,不過工作外,我還是希望能明白未定義的行為中那些必然性,o(︶︿︶)o 唉!因果論的孩子傷不起。等我下一篇博文分析lua原始碼中table的實現就能夠更加清晰的說明這些了。

---------------------------------------------------------------------------------------------分割線-----------------------------------------------------------------------------------------------------------

相關推薦

淺析Luatable刪除

當我在工作中使用lua進行開發時,發現在lua中有4種方式遍歷一個table,當然,從本質上來說其實都一樣,只是形式不同,這四種方式分別是: forkey, value in pairs(tbtest) do   XXX  endforkey, value in ipairs(tbtest) do 

【演算法】二叉樹前序、序、後序相互求法

二叉樹前序、中序、後序遍歷相互求法 原文地址      今天來總結下二叉樹前序、中序、後序遍歷相互求法,即如果知道兩個的遍歷,如何求第三種遍歷方法,比較笨的方法是畫出來二叉樹,然後根據各種遍歷不同的特性來求,也可以程式設計求出,下面我們分別說明。  

使用C#操作二叉樹的插入查詢列印程式碼

Node類: public class Node { public int Item { set; get; } //節點資料 public Node LeftChild { set; get; } //左子節點的引用

C# 的委托事件

per fir 位置 局部變量 意義 不容易 演示 很好 load 引言 委托 和 事件在 .Net Framework中的應用非常廣泛,然而,較好地理解委托和事件對很多接觸C#時間不長的人來說並不容易。它們就像是一道檻兒,過了這個檻的人,覺得真是太容易了,而沒有過去的人每

淺析Luatable

當我在工作中使用lua進行開發時,發現在lua中有4種方式遍歷一個table,當然,從本質上來說其實都一樣,只是形式不同,這四種方式分別是:   for key, value in pairs(tbtest) do 

七:重建二叉樹依據先序或者後序重建二叉樹

off 相同 tree int roo 節點 先序 throw -a 對於一顆二叉樹。能夠依據先序遍歷(或者後序遍歷)和中序遍歷(樹中不含反復的數字)又一次還原出二叉樹。 解析: 1. 先序遍歷序列的第一個元素必然是根節點,能夠由此獲取二叉樹的根節點。 2. 依

根據後序樹構造二叉樹

eno build 中序遍歷樹 oot post rsa uil cnblogs 找到 根據中序遍歷和後序遍歷樹構造二叉樹 樣例: 給出樹的中序遍歷: [1,2,3] 和後序遍歷: [1,3,2] 返回如下的樹: 2 / \ 1 3 借鑒上一篇《前序遍歷和中序遍

二叉樹先序後序

二叉樹 com size 基本 html 後序 href col spa 轉自:https://www.cnblogs.com/polly333/p/4740355.html 基本思想>>   先序遍歷:根——>左——>右   先序遍歷:左——>

數據結構35:二叉樹前序後序

tdi 代碼 nod 完成 循環 同時 reat pan 設置 遞歸算法底層的實現使用的是棧存儲結構,所以可以直接使用棧寫出相應的非遞歸算法。 先序遍歷的非遞歸算法 從樹的根結點出發,遍歷左孩子的同時,先將每個結點的右孩子壓棧。當遇到結點沒有左孩子的時候,取棧頂的右

72 後序樹構造二叉樹

實的 dong scrip size turn -c -h red 左右子樹 原題網址:https://www.lintcode.com/problem/construct-binary-tree-from-inorder-and-postorder-traversal/d

後序求類層序

PTA-ZigZagging on a Tree (25 分) Suppose that all the keys in a binary tree are distinct positive integers. A unique binary tree can be determin

7-5 還原二叉樹 25 分二叉樹,根據 先序

7-5 還原二叉樹 (25 分) 給定一棵二叉樹的先序遍歷序列和中序遍歷序列,要求計算該二叉樹的高度。 輸入格式: 輸入首先給出正整數N(≤50),為樹中結點總數。下面兩行先後給出先序和中序遍歷序列,均是長度為N的不包含重複英文字母(區別大小寫)的字串。 輸出格式:

二叉樹的之先序後序

例圖:                   1.先序遍歷   先序遍歷也叫做先跟遍歷、前序遍歷。先序遍歷步驟為:訪問根結點然後遍歷左子樹,最後遍歷右子樹。在遍歷左、右子樹時,仍然先訪問根結點,然後遍歷左子樹,最後遍歷右子樹。即根左右。   如上圖1,先序遍歷的序列為:

java編寫二叉樹以及前序後序

/** * 實現二叉樹的建立、前序遍歷、中序遍歷和後序遍歷 **/ package DataStructure; /** * Copyright 2014 by Ruiqin Sun * All right reserved * created on 2014

已知二叉樹的前序,如何求後序

昨天ACM集訓的時候出現了這道題,沒接觸過半天都沒做出來,但看到解法還是挺好理解的。 一道HULU的筆試題(How I wish yesterday once more) 假設有棵樹,長下面這個樣子,它的前序遍歷,中序遍歷,後續遍歷都很容易知道。 PreOr

根據後序重建二叉樹

二叉樹的重建 二叉樹的重建方法: 一、根據前序加中序遍歷重建二叉樹 構造該二叉樹的過程如下: 1. 根據前序序列的第一個元素建立根結點; 2. 在中序序列中找到該元素,確定根結點的左右子樹的中序序列;

分別根據前歷來推二叉樹的結構

1、理論分析: 資料結構的基礎知識中重要的一點就是能否根據兩種不同遍歷序列的組合(有三種:先序+中序,先序+後序,中序+後序),唯一的確定一棵二叉樹。然後就是根據二叉樹的不同遍歷序列(先序、中序、後序

1建立二叉樹的二叉連結串列。 2寫出對用二叉連結串列儲存的二叉樹進行先序、後序遍歷的遞迴非遞迴演算法。 3寫出對用二叉連結串列儲存的二叉樹進行層次遍歷演算法。 4求二叉樹的所有葉子及結點總數。

(1)建立二叉樹的二叉連結串列。 (2)寫出對用二叉連結串列儲存的二叉樹進行先序、中序和後序遍歷的遞迴和非遞迴演算法。 (3)寫出對用二叉連結串列儲存的二叉樹進行層次遍歷演算法。(4)求二叉樹的所有葉子及結點總數。 include<stdio.h> #inclu

【LeetCode】根據前序建樹 && 根據後續建樹

Total Accepted: 7041 Total Submissions: 27696 My Submissions Given preorder and inorder traversal of a tree, construct the binary tree.

先序/後序構建二叉樹

1:問題 給定二叉樹的2個遍歷序列(如先序+中序,先序+後序,中序+後序等),是否能夠根據這2個遍歷序列唯一確定二叉樹? struct BinaryTreeNode { int m_nValue; BinaryTreeNode* m_pLeft; BinaryTree