10/1/2008 4:09:00 AM

該如何學好 "寫程式" #2. 為什麼 programmer 該學資料結構 ??

543 | C# | MSDN | 小技巧 | 安德魯的當年勇 | 我的作品 | 技術隨筆 | Microsoft.NET | [精選文章]

自從貼了上一篇 [該如何學好 "寫程式"] 一文,原本以為這種老生常談的內容沒什麼人會看,沒想到還有人給我回應.. :D 原來這種文章還是有市場的。接下來這篇,是延續上一篇,來談談要成為合格的 programmer, 我認為應該要俱備的 "內功" 是什麼。上篇我提到,我認知的 programmer,就是要有實作 (CODING) 的能力。要有能力把技術規格 (像是輸入格式,操作介面等等) 具體的寫成可以執行的程式碼。當然是要寫的又快又好,穩定不當機又沒 BUG ...。

 

在這個階段 (programmer),會一些具體的工具或是技術是必要的,但是它絕對不是主角。如何去運用你的工具才是關鍵。我認為 "資料結構" 就是能正確運用你的工具 (程式語言及函式庫) 最重要的知識。我常看到很多會一堆 "先進" 的技術,卻寫出很可笑的 code ... 。這種例子太多了,兩層迴圈擺錯順序,或是某些動作 *不小心* 擺到迴圈內,多花了好幾倍的時間在做冤枉事...。這種例子我通通把它規在基本功夫不好,或是常聽的邏輯觀念不佳。所以在上一篇我會提到,好的 programmer 至少能滿足我講的三個基本要求:

  1. 丟一付洗過的撲克牌給你 (不要多,黑桃1 ~ 13就好),你知道怎麼用 Bubble Sort / Quick Sort 的步驟把它排好嗎? 丟一個陣列,裡面隨便打幾個數字,你能寫程式把它由小到大排好印出來嗎?
  2. 假設記憶體夠大的話,你有辦法把一百萬筆通訊錄資料讀到記憶體內 (用什麼物件都隨你),然後還能用很快的速度找到你要的資料嗎? 不同的搜尋方式,你知道該用什麼樣的方式找才有效率嗎?
  3. 以台灣高速公路為題 (中山高、北二高、國道二號),你有辦法寫程式,讓使用者指定起點跟終點的交流道,然後替它找出建議的路線嗎? (把延路經過的交流到跟收費站列出來就好)

第一個只要你知道排序的方法,剩下的就是你有沒有本事把腦袋的想法寫成 CODE 而以。這個要求大部份的人都能過關,我就不多作解釋了。來看看第二個要求,它考驗的是你該用什麼樣的方式 "SEARCH" ?

我就以 C# 為例來說明這個問題該怎樣思考。以資訊系的 "資料結構" 這門課的角度來思考,你應該要找出個適合的資料結構 (Binary Tree, Heap, Linked List ... etc) 來存放這堆資料。不過資料結構這麼多種,你都要自己做嗎? .NET framework 已經在 System.Collection.Generic 這命名空間內提供了一堆好用的 Collection 給你用了,你該怎麼挑選才好? 課堂上老師不會教你實作的東西,而公司的前輩也不會教你這種基礎的東西,那你該怎麼把這兩者應用在一起?

就先從 (2) 的例子開始吧! 通訊錄最基本的要求,就是儲存的資料要能按照姓名/EMAIL/電話號碼排序。輸入名字後,要能很快的找到這個人完整的通訊錄。如果能像手機一樣,邊輸入名字就邊過濾名單,直到名字打完人就找到的話更好。在宣告了 class ContactData { ... } 類別來處理一筆資料後,下一步你會怎麼做?

 

ContactData 類別定義[copy code]
        public class ContactData        {            public string Name;            public string EmailAddress;            public string PhoneNumber;            public void OutputData(TextWriter writer)            {                writer.WriteLine("Name:\t{0}", this.Name);                writer.WriteLine("Email:\t{0}", this.EmailAddress);                writer.WriteLine("Phone:\t{0}", this.PhoneNumber);                writer.WriteLine();            }        }
   1:  public class ContactData
   2:  {
   3:      public string Name;
   4:      public string EmailAddress;
   5:      public string PhoneNumber;
   6:      public void OutputData(TextWriter writer)
   7:      {
   8:          writer.WriteLine("Name:\t{0}", this.Name);
   9:          writer.WriteLine("Email:\t{0}", this.EmailAddress);
  10:          writer.WriteLine("Phone:\t{0}", this.PhoneNumber);
  11:          writer.WriteLine();
  12:      }
  13:  }

 

開始來看看,有基本功夫的 programmer 跟一般 "熟 C# 熟 .NET" 的 programmer 差在那裡吧! 程式很簡單,先產生一百萬筆假資料,然後去找 A123456 這個人的資料,接著再找出手機號碼為 0928-1234 開頭的所有人資料。事後會分別計算花掉的時間跟程式佔用的記憶體大小。

 

1. 大概有 70% 的人,會選擇用 List<ContactData>,不為什麼,只因為他沒想到別的方法,或是直覺就覺的要這樣寫... 來看看這樣的 code:

用 List<ContactData> 寫的範例程式[copy code]
        private static void Sample1()        {            Stopwatch timer = new Stopwatch();            timer.Reset();            timer.Start();            // 產生假資料庫            List<ContactData> contacts = new List<ContactData>();            {                for (int index = 999999; index >= 0; index--)                {                    ContactData cd = new ContactData();                    cd.Name = string.Format("A{0:D6}", index);                    cd.EmailAddress = string.Format("{0:D6}@chicken-house.net", index);                    cd.PhoneNumber = string.Format("0928-{0:D6}", index);                    contacts.Add(cd);                }            }            Console.WriteLine("建資料花了: {0} ticks ({1} msec)", timer.ElapsedTicks, timer.ElapsedMilliseconds);            timer.Reset();            timer.Start();            {                // 搜尋 A123456 這個人的資料                ContactData data = null;                data = contacts.Find(delegate(ContactData x) { return x.Name == "A123456"; });                Console.WriteLine("搜尋 A123456 花了: {0} ticks ({1} msec)", timer.ElapsedTicks, timer.ElapsedMilliseconds);                //data.OutputData(Console.Out);            }            timer.Reset();            timer.Start();            {                // 列出電話號碼為 0928-1234* 開頭的使用者                foreach (ContactData match in contacts.FindAll(delegate(ContactData x) { return x.PhoneNumber.StartsWith("0928-1234"); }))                {                    //match.OutputData(Console.Out);                }                Console.WriteLine("搜尋 0928-1234* 資料花了: {0} ticks ({1} msec)", timer.ElapsedTicks, timer.ElapsedMilliseconds);            }            Console.WriteLine("共使用記憶體: {0}MB", Environment.WorkingSet / 1000000);        }
   1:  private static void Sample1()
   2:  {
   3:      Stopwatch timer = new Stopwatch();
   4:      timer.Reset();
   5:      timer.Start();
   6:      // 產生假資料庫
   7:      List<ContactData> contacts = new List<ContactData>();
   8:      {
   9:          for (int index = 999999; index >= 0; index--)
  10:          {
  11:              ContactData cd = new ContactData();
  12:              cd.Name = string.Format("A{0:D6}", index);
  13:              cd.EmailAddress = string.Format("{0:D6}@chicken-house.net", index);
  14:              cd.PhoneNumber = string.Format("0928-{0:D6}", index);
  15:              contacts.Add(cd);
  16:          }
  17:      }
  18:      Console.WriteLine("建資料花了: {0} ticks ({1} msec)", timer.ElapsedTicks, timer.ElapsedMilliseconds);
  19:      timer.Reset();
  20:      timer.Start();
  21:      {
  22:          // 搜尋 A123456 這個人的資料
  23:          ContactData data = null;
  24:          data = contacts.Find(delegate(ContactData x) { return x.Name == "A123456"; });
  25:          Console.WriteLine("搜尋 A123456 花了: {0} ticks ({1} msec)", timer.ElapsedTicks, timer.ElapsedMilliseconds);
  26:          //data.OutputData(Console.Out);
  27:      }
  28:      timer.Reset();
  29:      timer.Start();
  30:      {
  31:          // 列出電話號碼為 0928-1234* 開頭的使用者
  32:          foreach (ContactData match in contacts.FindAll(delegate(ContactData x) { return x.PhoneNumber.StartsWith("0928-1234"); }))
  33:          {
  34:              //match.OutputData(Console.Out);
  35:          }
  36:          Console.WriteLine("搜尋 0928-1234* 資料花了: {0} ticks ({1} msec)", timer.ElapsedTicks, timer.ElapsedMilliseconds);
  37:      }
  38:      Console.WriteLine("共使用記憶體: {0}MB", Environment.WorkingSet / 1000000);
  39:  }

 

憑良心說,寫的出這樣程式碼的人,已經算是高手了。因為這樣已經用到不少高級技巧,像是 delegate, anonums method, 還有知道 List<T>.Find( ) 怎麼用等等... 以下是他的執行結果:

image

 

 

2. 更進階一點的人 (另外 25%),也許會額外加上 Dictionary 當作索引,來改善 search A123456 這筆資料的效率...

加上 Dictionary 當作索引的 code[copy code]
            // 略            // 產生假資料庫            Dictionary<string, ContactData> name_index = new Dictionary<string, ContactData>();            List<ContactData> contacts = new List<ContactData>();            {                for (int index = 999999; index >= 0; index--)                {                    ContactData cd = new ContactData();                    cd.Name = string.Format("A{0:D6}", index);                    cd.EmailAddress = string.Format("{0:D6}@chicken-house.net", index);                    cd.PhoneNumber = string.Format("0928-{0:D6}", index);                    name_index.Add(cd.Name, cd);                    contacts.Add(cd);                }            }            Console.WriteLine("建資料花了: {0} ticks ({1} msec)", timer.ElapsedTicks, timer.ElapsedMilliseconds);            timer.Reset();            timer.Start();            {                // 搜尋 A123456 這個人的資料                ContactData data = name_index["A123456"];                //data = contacts.Find(delegate(ContactData x) { return x.Name == "A123456"; });                Console.WriteLine("搜尋 A123456 花了: {0} ticks ({1} msec)", timer.ElapsedTicks, timer.ElapsedMilliseconds);                //data.OutputData(Console.Out);            }           // 略
   1:   // 略
   2:   // 產生假資料庫
   3:   Dictionary<string, ContactData> name_index = new Dictionary<string, ContactData>();
   4:   List<ContactData> contacts = new List<ContactData>();
   5:   {
   6:       for (int index = 999999; index >= 0; index--)
   7:       {
   8:           ContactData cd = new ContactData();
   9:           cd.Name = string.Format("A{0:D6}", index);
  10:           cd.EmailAddress = string.Format("{0:D6}@chicken-house.net", index);
  11:           cd.PhoneNumber = string.Format("0928-{0:D6}", index);
  12:           name_index.Add(cd.Name, cd);
  13:           contacts.Add(cd);
  14:       }
  15:   }
  16:   Console.WriteLine("建資料花了: {0} ticks ({1} msec)", timer.ElapsedTicks, timer.ElapsedMilliseconds);
  17:   timer.Reset();
  18:   timer.Start();
  19:   {
  20:       // 搜尋 A123456 這個人的資料
  21:       ContactData data = name_index["A123456"];
  22:       //data = contacts.Find(delegate(ContactData x) { return x.Name == "A123456"; });
  23:       Console.WriteLine("搜尋 A123456 花了: {0} ticks ({1} msec)", timer.ElapsedTicks, timer.ElapsedMilliseconds);
  24:       //data.OutputData(Console.Out);
  25:   }
  26:  // 略

 

會寫到這樣,也算強者了。不但對 C# 夠熟,也有用 Collection 物件來當作索引的觀念。程式碼只有兩行不同,一個是多宣告了 Dictionary 物件 (第3行),另一個是搜尋的地方 (第21行)。來看看執行結果:

image

 

果然有效,建資料從 5151ms 變成 5843ms, 記憶體從 288MB 變成 340MB,不過 search(A123456) 卻快的嚇人, 0ms... 破錶了!

 

但是這樣的 CODE 老實說只能算是及格而以,因為它沒有挑對 Collection 來用。怎麼說? 我的理由有這幾個:

  1. List<T> 的搜尋效能不好
  2. 沒能滿足用多種排序方式的要求 (需要時要當場執行 List<T>.Sort( ))

如果這是某個 Mail Client 內的 CODE,產品經理一定會問:

"如果資料從一百萬筆,變成一億筆,程式的表現會是什麼情況?"

 

有沒有基本功夫,這裡開始有差別了。唸過資料結構的都知道有個叫 "時間複雜度" (time complexity) 的東西,用 O(n) 表示。O(n) 代表花費的時間會跟資料比數成線性的成長。100倍的資料大概就要花上100倍的時間.. 如果是 O(n^2) 的演算法,則 100 倍的資料就會花上 10000 倍的時間。

MSDN 專業的地方就在這裡。Microsoft 真的很細心的在每一個 Collection 物件的說明文件上,都會標上 time complexity。有唸書有保佑,瞄到那行字我的問題就都解決掉了。先來看看 List<T> 的行為:

 

List<T>.Add(T item)

If Count is less than Capacity, this method is an O(1) operation. If the capacity needs to be increased to accommodate the new element, this method becomes an O(n) operation, where n is Count.

 

List<T>.FindAll(Predicate<T> match)

This method performs a linear search; therefore, this method is an O(n) operation, where n is Count.

 

再來看看 Dictionary<TKey, TValue> 的行為:

Dictionary<TKey, TValue>.Add(TKey key, TValue value)

If Count is less than the capacity, this method approaches an O(1) operation. If the capacity must be increased to accommodate the new element, this method becomes an O(n) operation, where n is Count.

 

Dictionary<TKey, TValue>.Item[TKey key] {get; set;}

Getting or setting the value of this property approaches an O(1) operation.

 

好,答案出來了。當資料變成一百倍時,List.Add 是 O(1), 所以每加一筆資料的時間不會越來越久 (safe). 但是搜尋時間是 O(n), 意思是現在找 A123456 要花 60ms, 未來有一億筆就要花 60x100=6000ms=6sec, 找 0928-1234* 則要花 240x100=24000ms=24sec... 以這樣的成長速度,記憶體還沒用完,你的程式就會慢到受不了了。有沒有其它的解決辦法?

 

換成 Dictionary 就酷多了,搜尋時間是 O(1), 代表不管你有幾筆,搜尋的時間都差不多。為什麼? MSDN 說的很清楚...

http://msdn.microsoft.com/en-us/library/xfhwa508.aspx

The Dictionary<(Of <(TKey, TValue>)>) generic class provides a mapping from a set of keys to a set of values. Each addition to the dictionary consists of a value and its associated key. Retrieving a value by using its key is very fast, close to O(1), because the Dictionary<(Of <(TKey, TValue>)>) class is implemented as a hash table.

 

什麼是 HashTable? 又是一個好例子,唸過資料結構的都知道吧? 我就不多說了,請看 wiki:

http://en.wikipedia.org/wiki/Hashtable

 

一樣是看 MSDN 文件,有沒有唸過資料結構,到這裡就差這麼多了。體會到學校教的東西真的有用了嗎? 這個例子還沒完,再看下去。

 

事實上,以上的實作方式都不合格。LIST 效能不好,Dictionary 拿來作索引有個致命的缺點:

它的 KEY 不能重複!!!

是的,對應到資料庫的話,它就好像是個 unique key 一樣。拿來當 NAME 的索引還沒問題,拿來當其它欄位的索引就糟糕了,別說效能問題,連用都不能用。

另外,針對排序的問題也是無解,這是 HashTable 的特性,要照順序排,就要另請高明。

 

事實上,以上的實作方式都不合格。List 搜尋的效能不算好,而 Dictionary 也只能處理 exact match 的狀況,同時也無法處理需要排序的問題。

 

唸過書的再想想,這時該怎麼辦?

標準解法是分別照這幾個欄位排序,然後用 Binary Search. 這才是正解。因為排序好的資料就像一般資料庫的 index, 可以讓你很容易的 order by, 同時又能讓你很快的找到你要的資料,甚至是列出某一段範圍的資料都沒問題。

不過寫成程式要怎麼作? MSDN 就在手上嘛,System.Collection.Generic 就把它當購物網站,逛一逛... 看有沒有其它合用的。

 

不錯,又找到兩個: SortedListSortedDictionary,還是一樣,那一個比較合適? MSDN 都寫的很清楚,足夠你判斷了,前題是資料結構教的幾個基本觀念 (像是前面講的 Hash Table, Time Complexity 等) 人家寫出來你要看的懂,看的懂就知道該挑那一個。

 

至於挑選的過程我就不多說了。我最後決定用 SortedList, 列一下這個 Collection 的特性:

SortedList.Add( )

This method is an O(n) operation for unsorted data, where n is Count. It is an O(log n) operation if the new element is added at the end of the list. If insertion causes a resize, the operation is O(n).

 

新增一筆需要的時間是 O(n), 唯一特例是加在最後面,而且沒引發 resize 的動作,就是 O(log n)。至於排序? 通通是 O(1),因為在 Add() 把資料加進來時就排好序了,所以 Add() 花的 O(log n) 就是在排序。要照順序印資料或找資料,完全不費吹灰之力,拿來印就是了。不過比較可惜的是,SortedList 並沒有提供 BinarySearch,因此要找 "0928-1234*" 這樣的資料要辛苦點,自己用 BinarySearch 的邏輯,簡單寫一下吧。如果前面的關卡都過了,這應該不難吧?

改用 SortedList 最大的缺點就是載入資料時會比較慢,不過其它在程式的處理上,還有效能都更貼近這個題目的需求。來看看程式碼,這次我用了兩個 SortedList,分別代表替 name 及 phone number 作排序:

 

改用 SortedList<> 的範例[copy code]
        private static void Sample3()        {            Stopwatch timer = new Stopwatch();            timer.Reset();            timer.Start();            // 產生假資料庫            SortedList<string, ContactData> name_index = new SortedList<string, ContactData>();            SortedList<string, ContactData> phone_index = new SortedList<string, ContactData>();            {                for (int index = 0; index < 1000000; index++)                {                    ContactData cd = new ContactData();                    cd.Name = string.Format("A{0:D6}", index);                    cd.EmailAddress = string.Format("{0:D6}@chicken-house.net", index);                    cd.PhoneNumber = string.Format("0928-{0:D6}", index);                    name_index.Add(cd.Name, cd);                    phone_index.Add(cd.PhoneNumber, cd);                }            }            Console.WriteLine("建資料花了: {0} ticks ({1} msec)", timer.ElapsedTicks, timer.ElapsedMilliseconds);            timer.Reset();            timer.Start();            {                // 搜尋 A123456 這個人的資料                ContactData data = name_index["A123456"];                Console.WriteLine("搜尋 A123456 花了: {0} ticks ({1} msec)", timer.ElapsedTicks, timer.ElapsedMilliseconds);                //data.OutputData(Console.Out);            }            timer.Reset();            timer.Start();            {                // 列出電話號碼為 0928-1234* 開頭的使用者                for (int pos = BinarySearch<string, ContactData>(phone_index, "0928-1234");                    pos < BinarySearch<string, ContactData>(phone_index, "0928-1235");                    pos++)                {                    //phone_index.Values[pos].OutputData(Console.Out);                }                Console.WriteLine("搜尋 0928-1234* 資料花了: {0} ticks ({1} msec)", timer.ElapsedTicks, timer.ElapsedMilliseconds);            }            Console.WriteLine("共使用記憶體: {0}MB", Environment.WorkingSet / 1000000);        }        private static int BinarySearch<TKey, TValue>(SortedList<TKey, TValue> index, TKey key)        {            return BinarySearch<TKey, TValue>(index, key, 0, index.Count - 1);        }        private static int BinarySearch<TKey, TValue>(SortedList<TKey, TValue> index, TKey key, int start, int end)        {            if (start == end) return end;            int pos = (start + end) / 2;            int compareResult = index.Comparer.Compare(key, index.Keys[pos]);            if (compareResult == 0)            {                return pos;            }            else if (compareResult > 0)            {                return BinarySearch<TKey, TValue>(index, key, pos + 1, end);            }            else            {                return BinarySearch<TKey, TValue>(index, key, start, pos - 1);            }        }
   1:  private static void Sample3()
   2:  {
   3:      Stopwatch timer = new Stopwatch();
   4:      timer.Reset();
   5:      timer.Start();
   6:      // 產生假資料庫
   7:      SortedList<string, ContactData> name_index = new SortedList<string, ContactData>();
   8:      SortedList<string, ContactData> phone_index = new SortedList<string, ContactData>();
   9:      {
  10:          for (int index = 0; index < 1000000; index++)
  11:          {
  12:              ContactData cd = new ContactData();
  13:              cd.Name = string.Format("A{0:D6}", index);
  14:              cd.EmailAddress = string.Format("{0:D6}@chicken-house.net", index);
  15:              cd.PhoneNumber = string.Format("0928-{0:D6}", index);
  16:              name_index.Add(cd.Name, cd);
  17:              phone_index.Add(cd.PhoneNumber, cd);
  18:          }
  19:      }
  20:      Console.WriteLine("建資料花了: {0} ticks ({1} msec)", timer.ElapsedTicks, timer.ElapsedMilliseconds);
  21:      timer.Reset();
  22:      timer.Start();
  23:      {
  24:          // 搜尋 A123456 這個人的資料
  25:          ContactData data = name_index["A123456"];
  26:          Console.WriteLine("搜尋 A123456 花了: {0} ticks ({1} msec)", timer.ElapsedTicks, timer.ElapsedMilliseconds);
  27:          //data.OutputData(Console.Out);
  28:      }
  29:      timer.Reset();
  30:      timer.Start();
  31:      {
  32:          // 列出電話號碼為 0928-1234* 開頭的使用者
  33:          for (int pos = BinarySearch<string, ContactData>(phone_index, "0928-1234");
  34:              pos < BinarySearch<string, ContactData>(phone_index, "0928-1235");
  35:              pos++)
  36:          {
  37:              //phone_index.Values[pos].OutputData(Console.Out);
  38:          }
  39:          Console.WriteLine("搜尋 0928-1234* 資料花了: {0} ticks ({1} msec)", timer.ElapsedTicks, timer.ElapsedMilliseconds);
  40:      }
  41:      Console.WriteLine("共使用記憶體: {0}MB", Environment.WorkingSet / 1000000);
  42:  }
  43:  private static int BinarySearch<TKey, TValue>(SortedList<TKey, TValue> index, TKey key)
  44:  {
  45:      return BinarySearch<TKey, TValue>(index, key, 0, index.Count - 1);
  46:  }
  47:  private static int BinarySearch<TKey, TValue>(SortedList<TKey, TValue> index, TKey key, int start, int end)
  48:  {
  49:      if (start == end) return end;
  50:      int pos = (start + end) / 2;
  51:      int compareResult = index.Comparer.Compare(key, index.Keys[pos]);
  52:      if (compareResult == 0)
  53:      {
  54:          return pos;
  55:      }
  56:      else if (compareResult > 0)
  57:      {
  58:          return BinarySearch<TKey, TValue>(index, key, pos + 1, end);
  59:      }
  60:      else
  61:      {
  62:          return BinarySearch<TKey, TValue>(index, key, start, pos - 1);
  63:      }
  64:  }

 

執行結果:

image

 

至於前面產品經理問的問題,各位就試著自己到 MSDN 找看看答案吧! 比較過之後,你就會瞭解為什麼我會挑選 SortedList .. 我只挑 SEARCH 時間來看,List 的搜尋是 O(n), 而 SortedList 的搜尋是排序過的資料作 BinarySearch, 找找書就知道是 O(log n), 分別來比較一下:

當 N 等於 1000000 時:

List: 3131861 ticks
SortedList: 39294 ticks (快 80 倍)

 

推算一下,N 放大為 100 倍 (100000000) 時:

List: 3131861 x 100000000 / 1000000 = 313186100 ticks
SortedList: 39294 x log 100000000 / log 1000000 = 52392 ticks (快 5978 倍)

 

看到了嗎? 換個 Collection 物件,對於 Search 這個動作,一百萬筆資料時差了 80 倍,當資料成長一百倍 (100000000 筆) 時,搜尋速度差異爆增為近 6000 倍! 這就是資料結構或是演算法的差異,這樣的差異已經大到其它地方最佳化怎麼作都補不回來的地步,唯一一個關鍵就是要用對演算法!

   

終於打完這篇了。沒想到前一篇寫一堆老生常談的話,這次又變成一堆 sample code 了。不過我的目的就是讓各位瞭解,基礎一定要顧好啊,不然寫程式是一定會碰到瓶頸的。這次從很簡單的需求,帶到資料結構的觀念,再帶到 MSDN 裡面特別標記的資訊...。看完後應該不會再有人說學校教的東西沒用了吧?

 

有網友問過我有沒有推薦什麼書? 很抱歉,我也只看過課本而以 ... 哈哈,這些純粹是出來工作後,無意間還想到要去翻翻課本得來的經驗。其實這種例子很多,過去我常貼的 multi-thread 的文章也是很多這樣的例子,只不過課本從資料結構換成作業系統了,這個主題才寫到 1-2, 後面還有, 有什麼看法或心得就請留在回應給我吧! 如果能支持一下旁邊的讚助商的話也算是種鼓厲啦.. 敬請期待下一篇..

--

調查一下,有人看這篇之前就知道 SortedList 嗎? 留個話給我吧,我很好奇這種東西有多少人會去用... :D



Comments

10/1/2008 9:36:44 AM #

Alan

您好

我31歲了,去年才開始學用C#寫程式,對寫程式有很大的興趣,也想把程式學好,所以平常買了很多的書在家自修,在公司也寫了一些程式上線使用中,可是總覺得對寫程式還是似懂非懂的感覺,在看了您的文章之後,才發覺或許是我從來沒學過資料結構以及您所提到的演算法,所以寫起程式來總好像有種「能執行、但又怪怪的」的感覺。也去光華搜尋過資料結構的相關書籍,但都只找到C/C++的,本想問您是否可以推薦一兩本C#的相關書籍,不過上面已經有答案了,呵!那我還是自己找找看好了。

您在「黑暗執行緒」裡的程式範例,我目前正在研究中,不過大部分還看不太懂,有問題再跟您請教,好嗎?感謝您寫的文章,對於我這個寫程式的菜鳥來說,有很大的幫助哦!謝謝。

Alan Taiwan | Reply

10/1/2008 11:01:15 AM #

chicken

找找 "data structure using c#" 吧,有找到幾本,不知道有沒有中譯版?

不然 MSDN 也有一小段:
msdn.microsoft.com/.../ms364091(VS.80).aspx

chicken Taiwan | Reply

10/1/2008 12:36:12 PM #

小熊子

一直在 Google Reader 潛水,一直翻到最後一句…不得不浮上來…
SortedList 很好用哩,我只是沒有仔細去比較,看 MSDN 的 O(1) O(N) 就拿來用了。
我當時直覺是懶,搞個 List 還不能 obj[xxx] 抓到我要的資料,還要 Find or IndexOf ,機車…
又開始了懶熊的性格,後來找到 SortedList 讚!!

小熊子 Taiwan | Reply

10/1/2008 6:27:14 PM #

Kenny

我知道有SortedList,但是我從來沒有機會用到它,或許是我的經驗還不足吧!

知道它的原因是我在解決一些問題的時候會看一下MSDN或Intellisense,所以知道它的存在。

Kenny Taiwan | Reply

10/1/2008 7:57:15 PM #

chicken

補了一小段,update 一下 :D

chicken Taiwan | Reply

10/4/2008 11:54:38 PM #

Stephen

您不仅程序写得好,文笔也好,用生动的文笔在一些看似简单的问题上做深入的讲解。非常喜欢您的文章,谢谢!

Stephen Australia | Reply

10/7/2008 10:31:25 AM #

KEN

謝謝您又補了這一長篇又精闢入裡的文章來釋義,
總算有點方向知道該怎麼做了,
也更知道自己有多才疏學淺了,
小弟幾乎都不知道您提及的技術及資料結構用法。

非常謝謝,我會努力多按幾次AD的。;)

P.S:既然市面上沒有此類書,您的文筆又這麼好且有實務經驗搭配,實在比教條式的書來得實用多了,有沒有考慮乾脆自己寫一本? ;) 我會很期待的。

KEN Taiwan | Reply

10/8/2008 12:25:08 PM #

chicken

KEN:

謝謝支持!

寫書我沒這打算,一來題材沒大到湊的到一本書,二來這種書也賺不到什麼錢吧? 哈哈..
第三最重要的一點,我想到要寫整本書就懶了 :P ,光是兩個月想投稿一篇文章就辦不到了,老是拖到三個月... 更別提寫書了

不過我 BLOG 還是會繼續寫啦 :D

chicken Taiwan | Reply

10/10/2008 2:11:23 AM #

chicken

真糟糕,同事告訴我我才發現,SortedList 的 KEY 也是不能重複...
不過也因為這樣,找到了其它更適合的 solution .. 我整理一下再貼出來... :P

chicken Taiwan | Reply

10/14/2008 4:15:11 PM #

小雷

小弟深有同感,一直以為用不到學校教的東西。但是程式愈寫愈多才發現,其實自已一直
在用以前學校教的東西在學習新技術及觀念。

小雷 Taiwan | Reply

10/14/2008 10:24:34 PM #

leem

SortedList我是看這篇文章後才知道的,謝謝您分享這麼好的文章。

leem Taiwan | Reply

10/15/2008 5:51:45 PM #

Johnny

我記得沒錯的, SortedList 應該早在王國榮的那本書就有提到了, 會用的人應該很多才對。

Johnny Taiwan | Reply

10/16/2008 7:25:05 PM #

chicken

其實 SortedList 什麼時後出來? .NET 1.0 就有非泛型的版本, .NET 2.0 開始支援 Generic 之後就有現在的版本了。真要說源頭,應該是當年 (2000?) 推出的 .NET Framework 1.0 SDK。

重點應該是要有能力判斷用的 LIB 適不適合,可以怎麼去找更合適的? 看到 object design patterns 譯者 (葉秉哲) 在書序裡講到一句話講的真好: "會用鐵鎚的人看到什麼都想敲下去,看到螺絲釘卻不知道還有更好用的螺絲起子一樣..."

會問誰用過只是純脆好奇統計一下而已 :P

chicken Taiwan | Reply

11/18/2008 2:54:18 PM #

salad

文章真的不錯, 首先要支持,
但我個人意見是在開發時, 如果沒太大機會使用上超大數據量,
而且老闆迫得很近, 還是使用直觀的方法去處理比較好,
快點完成工作, 成本考量來說是比較合理的.

不然用上一堆自己不習慣的, 而且只是服務十幾, 二十多條數據, 浪費了沒時間, 就不值了.

salad Macao S.A.R. | Reply

11/18/2008 8:15:14 PM #

chicken

感謝支持 :D
您講的沒錯! 跟本用不到的話就不需要考慮這些了。

會有動機寫這篇,是因為實際工作上碰到很多例子... 開發人員完全不注重資料結構,測試時建個三五筆資料,結果沒錯就交差了,結果數量一多 (合理的範圍內,不是巨量) 就跑不動了... Orz..

其實只要對資料結構有很基本的認知,就足以避免這問題了,能判斷出使用的作法,在合理範圍內會不會慢到跑不完也就夠了。

chicken Taiwan | Reply

11/25/2008 1:30:53 PM #

Litfal

您好,無意間逛到您的部落格,真的受益良多。
一開始是為了找些multiThread的文章,忽然就逛到了這裡,第一篇看的文章好像是有關於ThreadPool的吧。

對於寫程式為興趣、不是本系出身的我,看到你的這幾篇"如何學好寫程式"系列實在心有戚戚焉
資料結構與演算法對我來說完全陌生。
加上之前我大多使用VB6為開發工具,class的用法也很弱,正努力補強中。

現在轉戰.NET;最早繼續用ReDim X(n)管理動態陣列,後來雖改用ArrayList
對於Dictionary的知識也僅止於VB6的Collection。
直到拜讀了您的這篇文章。

今天花了一早上的時間實作眾多Collection,並與MSDN拼鬥,終於對
ArrayList、Hashtable、Dictionary、SortedList、SortedDictionary有了基礎的認識
但有了認識反而更迷惑了:
為什麼您選用SortedList而非SortedDictionary?

出自MSDN:
[quote]SortedList<(Of <(TKey, TValue>)>) uses less memory than SortedDictionary<(Of <(TKey, TValue>)>).

SortedDictionary<(Of <(TKey, TValue>)>) has faster insertion and removal operations for unsorted data, O(log n) as opposed to O(n) for SortedList<(Of <(TKey, TValue>)>).

If the list is populated all at once from sorted data, SortedList<(Of <(TKey, TValue>)>) is faster than SortedDictionary<(Of <(TKey, TValue>)>).
[/quote]

以您[u]改用 SortedList<> 的範例[/u]中,建立資料的方法為一筆一筆加入,SortedDictionary應該是比SortedList快上不少。
如果要使用SortedList一次加入較快的特性,應該先用範例二的Dictionary建立資料。
Dictionary<string, ContactData> name_index = new Dictionary<string, ContactData>();
再用SortedList去接收排序。
SortedList<string, ContactData> name_S_index = new SortedList<string, ContactData>(name_index);
類似這樣吧?(我是使用10^6個不重複亂數作為資料測試)(C#不太熟,語法有錯誤請見諒)

不過這樣都無法迴避鍵值無法重複的問題,我的想法是:
用Dictionary記錄鍵值=姓名,值=ContactData (與範例二相同)
用兩個List,List_p記錄電話,List_cd記錄ContactData
List_cd與List_p均隨List_p做sort,再對List_p作BinarySearch可得某範圍的電話
再對應到List_cd就是那範圍的人員了。

我自己也在寫部落格,記錄一些"成長的腳步"XD,可能會把使用眾多Collection的心得貼上,並加上參考文章來自此(網址)。

Litfal Taiwan | Reply

11/25/2008 7:34:47 PM #

Litfal

還有,不是我想挑骨頭(沒有興趣挑骨頭...研究別人寫的程式碼常常比自己寫還累),而是自己實作時遇到的一些問題:
您使用BinarySearch做"0928-1234"搜尋,但0928-1234*不只一筆,
以我的理解,您的BinarySearch的函數
if (compareResult == 0) return pos;
應該會使得搜尋到的pos不一定是0928-1234*中的第一筆或最後一筆
是否應該去掉這行,並把
else return BinarySearch<TKey, TValue>(index, key, start, pos - 1);
改成
else return BinarySearch<TKey, TValue>(index, key, start, pos);
傳回會只剩下if (start == end) return end;
這樣最後傳回的pos才一定是0928-1234*中的第一筆
同樣pos2=搜尋0928-1235也會得到第一筆
pos~(pos2-1)才是所有0928-1234*的範圍

如果理解有誤,請見諒

另外學到了function也可以用static宣告...
自己在做動態分析之類的(例如二項式係數的C(n取k)練習題)都在內部宣告靜態變數儲存結果...看到這就覺得又長了點知識

Litfal Taiwan | Reply

11/26/2008 7:36:06 PM #

chicken

很高興我寫的東西對你有幫助 :D 歡迎引用! 你提到的問題我回在底下:

1. BinarySearch
   如果 element 會重複,的確是要像你那樣調整。不過附帶的問題是所有呼叫的人
   都要去看看下一筆是不是也符合。這樣的話不如把 method 簽章改成這樣, 同時實作
   直接把所有符合的資料位置都抓出來..

   int[] BinarySearch<....>(....)

2. 我選用 SortedList 而不用 SortedDictionary 的原因很簡單,因為 SortedList
   本身的資料直接就是照順序儲存起來的。雖然 insert 花的時間慢一點,但是以我的
   應用例來說,照順序列出來的機會遠比新增資料的機會還大,我會優先考慮 read 的情況。

3. 其實你把問題想複雜了。解決的方法很簡單,要嘛解決 unique key 的問題(找個一定不會
   重複的值,如流水號),不然就全用物件來處理 (用個自定的類別把 Key / ContactData /
   Position 包成一包,放到 List 裡)。其它就直接用內建的 (或是 1 提到的) BinarySearch
   就可以搞定了


chicken Taiwan | Reply

11/26/2008 9:47:51 PM #

Litfal

興致沖沖的把以前寫的LIS練習題(Longest Increasing Subsequence)裡面用到的
Dim D() As type (C應該是寫成type D[] type為自訂Struct)
全部改寫成List<type>
發現效能反而變差...

純資料,只需要屬性,不需要內建方法
且已經知道會加入資料的筆數,可事先設定容量
List.Capacity = 10 ^ 6
ReDim D(10 ^ 6)
似乎還是陣列配合Struct的效率較佳?

Litfal Taiwan | Reply

11/27/2008 1:38:08 AM #

chicken

以效能來看,陣列當然最好。
以 time complexity 來看,除非 List 大小有變動,否則兩者是一樣的...

chicken Taiwan | Reply

3/15/2009 8:01:14 PM #

肉肉的打內工程師

很專業的好文,我是屬於非班科出生,最近正在準備念資工在職研,
這類比較深入淺出的文章對我這類人是十分有幫助的,
尤其我真的沒時間去好好K資料結構,可是目前又是朝著架構師的走向去走,
再看您的文章之前我大概停在Sample2的程度,現在又提升了一點。
其實這類演算法的效能問題我曾經也想過,在設計商業模型的時候也會注意會不會有明顯的白癡邏輯設計,
不過因為我們的資料量沒有大到ISP業者那種程度,而且我的案子的SQL幾乎都是從0開始,
因此只要不是愚蠢的設計,剛開始根本碰不到效能瓶頸,顧客也對產品很滿意,
數十個月後變慢了,我都是建議顧客升級硬體,而由於硬體通常會指定我們轉手,因此又可以賺一手。
而且如果後面要做軟體最佳化,可以再小賺一次。

肉肉的打內工程師 Taiwan | Reply

12/15/2009 4:23:07 PM #

Maxi

MCTS的入門書有各個collection物件的介紹
但MCTS要考完有看不完的書就是了...

不過這文章真的寫得很好,只是一般中小企業都沒有時間去做code refactoring就是了
選擇工具,就是要先做分析
如果需求是搜尋:加入是100萬:1, 那就應該用SortedList
如果是1:100萬的話就要選別的工具了
但話說回來,中小企業的軟體分析階段時程都短得可憐就是了...

Maxi Taiwan | Reply

12/18/2009 8:08:06 PM #

chicken867

其實我覺的,挑選正確的 collection 還不大需要到安排專門時間去作 "分析" 啦...
基本上開發的 programmer 本身就應該要有能力依據他開發的功能去挑選正確的 collection ..

我一直認為這些技能應該要變的跟反射動作一樣,想寫什麼樣的功能,很直覺就反應出來該用
什麼 collection 才對。比較模糊的地帶,或是有兩三種難以選擇的情況,才需要花點時間想一想。

chicken867 Taiwan | Reply

2/3/2010 2:21:12 PM #

muddy.lu

最近剛好看msdn用SortedList來cache住我的資料,方便往後搜尋
只是無聊好奇看看,網路上有沒有前輩有特殊用法,大大說得很多觀念真的受益良多
我才剛出來工作半年很多東西都來在摸索,很多也都還給課本
看完大大的文章,真該找個時間回去翻翻課本

muddy.lu Taiwan | Reply

Add comment


(Will show your Gravatar icon)

  Country flag

biuquote
Loading






精選文章

RUN! PC 文章及範例下載
2010/07. 結合檔案及資料庫的交易處理
2010/05. TxF讓檔案系統也能達到交易控制
2010/04. 生產者 vs 消費者 - 執行緒的供需問題
2008/11. 生產線模式的多執行緒應用
2008/09. 用ThreadPool發揮CPU運算能力
2008/06. SEMAPHORE在ASP.NET的應用
2008/04. 以ASP.NET開發同步WEB應用程式

如何學好 "寫程式" 系列
#1. 該如何學好 "寫程式" ??
#2. 為什麼 programmer 該學資料結構 ??
#3. 進階應用 - 資料結構 + 問題分析
#4. 你的程式夠 "可靠" 嗎?

#5. 善用 TRACE / ASSERT

安德魯是誰?

Andrew Wu | Create Your Badge

我喜歡鑽研物件導向、軟體工程及作業系統等相關技術。我會在這裡發表我的研究心得,也當作我自己的學習筆記。


Recent comments

Comment RSS