資料結構基礎溫故-1.線性表(下)
在上一篇中,我們瞭解了單鏈表與雙鏈表,本次將單鏈表中終端結點的指標端由空指標改為指向頭結點,就使整個單鏈表形成一個環,這種頭尾相接的單鏈表稱為單迴圈連結串列,簡稱迴圈連結串列(circular linked list)。
一、迴圈連結串列基礎
1.1 迴圈連結串列節點結構
迴圈連結串列和單鏈表的主要差異就在於迴圈的判斷條件上,原來是判斷p.next是否為空,現在則是p.next不等於頭結點,則迴圈未結束。
1.2 迴圈連結串列的O(1)訪問時間
在單鏈表中,有了頭結點,我們可以在O(1)時間訪問到第一個節點,但如果要訪問最後一個節點卻需要O(n)的時間,因為我們需要對整個連結串列進行一次遍歷。在迴圈連結串列中,我們可以藉助尾節點來實現,即不用頭指標,而是用指向終端結點的尾指標來表示迴圈連結串列
從上圖中可以看到,終端結點用尾指標(tail)指示,則查詢終端結點是O(1),而開始結點,其實就是tail.Next,其時間複雜也為O(1)。由此也可以聯想到,在合併兩個迴圈連結串列時,只需要修改兩個連結串列的尾指標即可快速地進行合併。
二、迴圈連結串列實現
2.1 迴圈連結串列節點的定義實現
public class CirNode<T> { public T Item { get; set; } public CirNode<T> Next { get; set; } public CirNode() { } public CirNode(T item) { this.Item = item; } }
這裡跟單鏈表的節點定義實現並無區別。
2.2 迴圈連結串列新節點的插入實現
public void Add(T value) { CirNode<T> newNode = new CirNode<T>(value);if (this.tail == null) { // 如果連結串列當前為空則新元素既是尾頭結點也是頭結點 this.tail = newNode; this.tail.Next = newNode; this.currentPrev = newNode; } else { // 插入到連結串列末尾處 newNode.Next = this.tail.Next; this.tail.Next = newNode; // 改變當前節點 if (this.currentPrev == this.tail) { this.currentPrev = newNode; } // 重新指向新的尾節點 this.tail = newNode; } this.count++; }
首先,這裡的currentPrev欄位是使用了前驅節點來標識當前節點,如要獲取當前節點的值可以通過currentPrev.Next.Item來獲得。其次,在最後將尾節點指標指向新插入的節點。
2.2 迴圈連結串列當前節點的移除實現
public void Remove() { if (this.tail == null) { throw new NullReferenceException("連結串列中沒有任何元素"); } else if (this.count == 1) { // 只有一個元素時將兩個指標置為空 this.tail = null; this.currentPrev = null; } else { if (this.currentPrev.Next == this.tail) { this.tail = this.currentPrev; } // 移除當前節點 this.currentPrev.Next = this.currentPrev.Next.Next; } this.count--; }
這裡考慮到刪除節點時必須尋找其前驅節點會導致連結串列進行遍歷,故使用了當前節點的前驅節點來標識這個當前節點。移除當前節點只需要currentPrev.Next = currentPrev.Next.Next即可。
以下是單迴圈連結串列的完整模擬實現程式碼,需要注意的是該CircularLinkedList主要是為下面的約瑟夫問題而設計,故只實現了一些很簡單的功能:
/// <summary> /// 單向迴圈連結串列的模擬實現 /// </summary> public class MyCircularLinkedList<T> { private int count; // 欄位:記錄資料元素個數 private CirNode<T> tail; // 欄位:記錄尾節點的指標 private CirNode<T> currentPrev; // 欄位:使用前驅節點標識當前節點 // 屬性:指示連結串列中元素的個數 public int Count { get { return this.count; } } // 屬性:指示當前節點中的元素值 public T CurrentItem { get { return this.currentPrev.Next.Item; } } public MyCircularLinkedList() { this.count = 0; this.tail = null; } public bool IsEmpty() { return this.tail == null; } // Method01:根據索引獲取節點 private CirNode<T> GetNodeByIndex(int index) { if (index < 0 || index >= this.count) { throw new ArgumentOutOfRangeException("index", "索引超出範圍"); } CirNode<T> tempNode = this.tail.Next; for (int i = 0; i < index; i++) { tempNode = tempNode.Next; } return tempNode; } // Method02:在尾節點後插入新節點 public void Add(T value) { CirNode<T> newNode = new CirNode<T>(value); if (this.tail == null) { // 如果連結串列當前為空則新元素既是尾頭結點也是頭結點 this.tail = newNode; this.tail.Next = newNode; this.currentPrev = newNode; } else { // 插入到連結串列末尾處 newNode.Next = this.tail.Next; this.tail.Next = newNode; // 改變當前節點 if (this.currentPrev == this.tail) { this.currentPrev = newNode; } // 重新指向新的尾節點 this.tail = newNode; } this.count++; } // Method03:移除當前所在節點 public void Remove() { if (this.tail == null) { throw new NullReferenceException("連結串列中沒有任何元素"); } else if (this.count == 1) { // 只有一個元素時將兩個指標置為空 this.tail = null; this.currentPrev = null; } else { if (this.currentPrev.Next == this.tail) { // 當刪除的是尾指標所指向的節點時 this.tail = this.currentPrev; } // 移除當前節點 this.currentPrev.Next = this.currentPrev.Next.Next; } this.count--; } // Method04:獲取所有節點資訊 public string GetAllNodes() { if (this.count == 0) { throw new NullReferenceException("連結串列中沒有任何元素"); } else { CirNode<T> tempNode = this.tail.Next; string result = string.Empty; for (int i = 0; i < this.count; i++) { result += tempNode.Item + " "; tempNode = tempNode.Next; } return result; } } }View Code
2.3 單迴圈連結串列的簡單測試
這裡的簡單測試主要涉及:1.順序插入5個節點,看節點元素是否正確;2.檢視當前節點是否正確;3.移除某個元素,檢視當前節點是否正確;測試程式碼如下所示:
static void MyCircularLinkedListTest() { MyCircularLinkedList<int> linkedList = new MyCircularLinkedList<int>(); // 順序插入5個節點 linkedList.Add(1); linkedList.Add(2); linkedList.Add(3); linkedList.Add(4); linkedList.Add(5); Console.WriteLine("All nodes in the circular linked list:"); Console.WriteLine(linkedList.GetAllNodes()); Console.WriteLine("--------------------------------------"); // 當前節點:第一個節點 Console.WriteLine("Current node in the circular linked list:"); Console.WriteLine(linkedList.CurrentItem); Console.WriteLine("--------------------------------------"); // 移除當前節點(第一個節點) linkedList.Remove(); Console.WriteLine("After remove the current node:"); Console.WriteLine(linkedList.GetAllNodes()); Console.WriteLine("Current node in the circular linked list:"); Console.WriteLine(linkedList.CurrentItem); // 移除當前節點(第二個節點) linkedList.Remove(); Console.WriteLine("After remove the current node:"); Console.WriteLine(linkedList.GetAllNodes()); Console.WriteLine("Current node in the circular linked list:"); Console.WriteLine(linkedList.CurrentItem); Console.WriteLine("--------------------------------------"); Console.WriteLine(); }
測試結果如下所示:
三、迴圈連結串列與約瑟夫問題
3.1 何為約瑟夫問題
據說著名猶太曆史學家 Josephus 有過以下的故事:在羅馬人佔領喬塔帕特後,39 個猶太人與Josephus及他的朋友躲到一個洞中,39個猶太人決定寧願死也不要被敵人抓到,於是決定了一個自殺方式,41個人排成一個圓圈,由第1個人開始報數,每報數到第3人該人就必須自殺,然後再由下一個重新報數,直到所有人都自殺身亡為止。然而 Josephus 和他的朋友並不想遵從,Josephus 要他的朋友先假裝遵從,他將朋友與自己安排在第16個與第31個位置,於是逃過了這場死亡遊戲。
以上就是著名的約瑟夫問題:N個人圍成一圈,從第一個開始報數,第M個將被殺掉,最後剩下Q個。從圍成一圈這裡就啟發了我們可以使用迴圈連結串列來解決該問題。
3.2 使用迴圈連結串列解決約瑟夫問題
(1)為CircularLinkedList新增Move()方法實現讓當前節點向前移動N步
public void Move(int step = 1) { if (step < 1) { throw new ArgumentOutOfRangeException("step", "移動步數不能小於1"); } for (int i = 1; i < step; i++) { currentPrev = currentPrev.Next; } }
注意到這裡迴圈是從1開始,因為currentPrev是當前節點的前驅節點,而不是真正的當前節點。
(2)在Main()方法中新增測試程式碼驗證是否能夠正確讓元素出列
static void JosephusTest() { MyCircularLinkedList<int> linkedList = new MyCircularLinkedList<int>(); string result = string.Empty; Console.WriteLine("Step1:請輸入人數N"); int n = Convert.ToInt32(Console.ReadLine()); Console.WriteLine("Step2:請輸入數字M"); int m = Convert.ToInt32(Console.ReadLine()); Console.WriteLine("Step3:報數遊戲開始"); // 新增參與人員元素 for (int i = 1; i <= n; i++) { linkedList.Add(i); } // 列印所有參與人員 Console.Write("所有參與人員:{0}", linkedList.GetAllNodes()); Console.WriteLine("\r\n" + "-------------------------------------"); result = string.Empty; while (linkedList.Count > 1) { // 依次報數:移動 linkedList.Move(m); // 記錄出隊人員 result += linkedList.CurrentItem + " "; // 移除人員出隊 linkedList.Remove(); Console.WriteLine(); Console.Write("剩餘報數人員:{0}", linkedList.GetAllNodes()); Console.Write(" 開始報數人員:{0}", linkedList.CurrentItem); } Console.WriteLine("\r\n" + "Step4:報數遊戲結束"); Console.WriteLine("出隊人員順序:{0}", result + linkedList.CurrentItem); }
執行結果下圖所示:
①N=10,M=4時:
②N=41,M=3時:
從上圖結果的出隊人員順序也可以看出,約瑟夫將自己和朋友安排在第16和第31個位置是在最後出隊的,就只剩他倆好基友了,死不死就不是猶太人說了算了,又可以風騷地在一起“搞基”了。
3.3 使用LinkedList<T>解決約瑟夫問題
在實際應用中,我們一般都會使用.NET中自帶的資料結構型別來解決一般問題,這裡我們就試著用LinkedList<T>來解決約瑟夫問題。
(1)定義一個Person類
public class Person { public int Id { get; set; } public string Name { get; set; } }
(2)初始化LinkedList集合
static LinkedList<Person> InitPersonList(int count) { LinkedList<Person> personList = new LinkedList<Person>(); for (int i = 1; i <= count; i++) { Person person = new Person(); person.Id = i; person.Name = "Counter-" + i.ToString(); personList.AddLast(person); } return personList; }
(3)由於LinkedList是雙向連結串列,但不是迴圈連結串列,因此這裡需要做一下判斷
static void JosephusTestWithLinkedList() { Console.WriteLine("請輸入人數N"); int n = Convert.ToInt32(Console.ReadLine()); Console.WriteLine("請輸入數字M"); int m = Convert.ToInt32(Console.ReadLine()); Console.WriteLine("-------------------------------------"); LinkedList<Person> linkedList = InitPersonList(n); LinkedListNode<Person> startNode = linkedList.First; LinkedListNode<Person> removeNode; while(linkedList.Count >= 1) { for (int i = 1; i < m; i++) { if (startNode != linkedList.Last) { startNode = startNode.Next; } else { startNode = linkedList.First; } } // 記錄出隊人員節點 removeNode = startNode; // 打印出隊人員ID號 Console.Write(removeNode.Value.Id + " "); // 確定下一個開始報數人員 if (startNode == linkedList.Last) { startNode = linkedList.First; } else { startNode = startNode.Next; } // 移除出隊人員節點 linkedList.Remove(removeNode); } Console.WriteLine(); }
這裡使用startNode記錄開始報數的人員節點,removeNode則記錄要出隊的人員節點。這裡在確定下一個開始報數人員時通過手動判斷LinkedList的當前節點是否已經達到了尾節點,如果是則轉到頭結點進行報數。最後將removeNode從LinkedList中移除即可。最終的執行結果如下圖所示:
①N=10,M=4時:
②N=41,M=3時:
PS:解決問題的思路和實現多種多樣,這裡給出的僅僅是最最普通的一種。
參考資料
(1)程傑,《大話資料結構》
(2)陳廣,《資料結構(C#語言描述)》
(3)段恩澤,《資料結構(C#語言版)》
作者:周旭龍
本文版權歸作者和部落格園共有,歡迎轉載,但未經作者同意必須保留此段宣告,且在文章頁面明顯位置給出原文連結。
相關推薦
資料結構基礎溫故-1.線性表(下)
在上一篇中,我們瞭解了單鏈表與雙鏈表,本次將單鏈表中終端結點的指標端由空指標改為指向頭結點,就使整個單鏈表形成一個環,這種頭尾相接的單鏈表稱為單迴圈連結串列,簡稱迴圈連結串列(circular linked list)。 一、迴圈連結串列基礎 1.1 迴圈連結串列節點結構 迴圈連結串列和單鏈表的
資料結構基礎溫故-1.線性表(中)
在上一篇中,我們學習了線性表最基礎的表現形式-順序表,但是其存在一定缺點:必須佔用一整塊事先分配好的儲存空間,在插入和刪除操作上需要移動大量元素(即操作不方便),於是不受固定儲存空間限制並且可以進行比較快捷地插入和刪除操作的連結串列橫空出世,所以我們就來複習一下連結串列。 一、單鏈表基礎 1.1 單鏈表的
資料結構基礎溫故-6.查詢(上):基本查詢與樹表查詢
只要你開啟電腦,就會涉及到查詢技術。如炒股軟體中查股票資訊、硬碟檔案中找照片、在光碟中搜DVD,甚至玩遊戲時在記憶體中查詢攻擊力、魅力值等資料修改用來作弊等,都要涉及到查詢。當然,在網際網路上查詢資訊就更加是家常便飯。查詢是計算機應用中最常用的操作之一,也是許多程式中最耗時的一部分,查詢方法的優劣對於系統的執
資料結構基礎溫故-6.查詢(下):雜湊表
雜湊(雜湊)技術既是一種儲存方法,也是一種查詢方法。然而它與線性表、樹、圖等結構不同的是,前面幾種結構,資料元素之間都存在某種邏輯關係,可以用連線圖示表示出來,而雜湊技術的記錄之間不存在什麼邏輯關係,它只與關鍵字有關聯。因此,雜湊主要是面向查詢的儲存結構。雜湊技術最適合的求解問題是查詢與給定值相等的記錄。
資料結構基礎之線性表(下)
轉自:http://www.cnblogs.com/edisonchou/p/4614934.html 線性表(下) 在上一篇中,我們瞭解了單鏈表與雙鏈表,本次將單鏈表中終端結點的指標端由空指標改為指向頭結點,就使整個單鏈表形成一個環,這種頭尾相接的單鏈表稱為單迴圈連結串列
資料結構基礎溫故-5.圖(中):圖的遍歷演算法
上一篇我們瞭解了圖的基本概念、術語以及儲存結構,還對鄰接表結構進行了模擬實現。本篇我們來了解一下圖的遍歷,和樹的遍歷類似,從圖的某一頂點出發訪問圖中其餘頂點,並且使每一個頂點僅被訪問一次,這一過程就叫做圖的遍歷(Traversing Graph)。如果只訪問圖的頂點而不關注邊的資訊,那麼圖的遍歷十分簡單,使用
資料結構基礎溫故-4.樹與二叉樹(下)
上面兩篇我們瞭解了樹的基本概念以及二叉樹的遍歷演算法,還對二叉查詢樹進行了模擬實現。數學表示式求值是程式設計語言編譯中的一個基本問題,表示式求值是棧應用的一個典型案例,表示式分為字首、中綴和字尾三種形式。這裡,我們通過一個四則運算的應用場景,藉助二叉樹來幫助求解表示式的值。首先,將表示式轉換為二叉樹,然後通過
資料結構基礎溫故-5.圖(中):最小生成樹演算法
圖的“多對多”特性使得圖在結構設計和演算法實現上較為困難,這時就需要根據具體應用將圖轉換為不同的樹來簡化問題的求解。 一、生成樹與最小生成樹 1.1 生成樹 對於一個無向圖,含有連通圖全部頂點的一個極小連通子圖成為生成樹(Spanning Tree)。其本質就是從連通圖任一頂點出發進行遍歷操作所經過
資料結構基礎溫故-5.圖(上):圖的基本概念
前面幾篇已經介紹了線性表和樹兩類資料結構,線性表中的元素是“一對一”的關係,樹中的元素是“一對多”的關係,本章所述的圖結構中的元素則是“多對多”的關係。圖(Graph)是一種複雜的非線性結構,在圖結構中,每個元素都可以有零個或多個前驅,也可以有零個或多個後繼,也就是說,元素之間的關係是任意的。現實生活中的很多
資料結構基礎溫故-4.樹與二叉樹(中)
在上一篇中,我們瞭解了樹的基本概念以及二叉樹的基本特點和程式碼實現,還用遞迴的方式對二叉樹的三種遍歷演算法進行了程式碼實現。但是,由於遞迴需要系統堆疊,所以空間消耗要比非遞迴程式碼要大很多。而且,如果遞迴深度太大,可能系統撐不住。因此,我們使用非遞迴(這裡主要是迴圈,迴圈方法比遞迴方法快, 因為迴圈避免了一系
資料結構基礎溫故-5.圖(下):最短路徑
圖的最重要的應用之一就是在交通運輸和通訊網路中尋找最短路徑。例如在交通網路中經常會遇到這樣的問題:兩地之間是否有公路可通;在有多條公路可通的情況下,哪一條路徑是最短的等等。這就是帶權圖中求最短路徑的問題,此時路徑的長度不再是路徑上邊的數目總和,而是路徑上的邊所帶權值的和。帶權圖分為無向帶權圖和有向帶權圖,但如
資料結構基礎溫故-4.樹與二叉樹(上)
前面所討論的線性表元素之間都是一對一的關係,今天我們所看到的結構各元素之間卻是一對多的關係。樹在計算機中有著廣泛的應用,甚至在計算機的日常使用中,也可以看到樹形結構的身影,如下圖所示的Windows資源管理器和應用程式的選單都屬於樹形結構。樹形結構是一種典型的非線性結構,除了用於表示相鄰關係外,還可以表示層次
資料結構基礎溫故-2.棧
現實生活中的事情往往都能總結歸納成一定的資料結構,例如餐館中餐盤的堆疊和使用,羽毛球筒裡裝的羽毛球等都是典型的棧結構。而在.NET中,值型別線上程棧上進行分配,引用型別在託管堆上進行分配,本文所說的“棧”正是這種資料結構。棧和佇列都是常用的資料結構,它們的邏輯結構與線性表相通,不同之處則在於操作受某種特殊限制
資料結構基礎溫故-3.佇列
在日常生活中,佇列的例子比比皆是,例如在車展排隊買票,排在隊頭的處理完離開,後來的必須在隊尾排隊等候。在程式設計中,佇列也有著廣泛的應用,例如計算機的任務排程系統、為了削減高峰時期訂單請求的訊息佇列等等。與棧類似,佇列也是屬於操作受限的線性表,不過佇列是隻允許在一端進行插入,在另一端進行刪除。在其他資料結構如
資料結構基礎溫故-7.排序
排序(Sorting)是計算機內經常進行的一種操作,其目的是將一組“無序”的記錄序列調整為按關鍵字“有序”的記錄序列。如何進行排序,特別是高效率地進行排序時計算機工作者學習和研究的重要課題之一。排序有內部排序和外部排序之分,若整個排序過程不需要訪問外存便能完成,則稱此類排序為內部排序,反之則為外部排序。本篇主
JavaScript 資料結構與演算法之美 - 線性表(陣列、棧、佇列、連結串列)
前言 基礎知識就像是一座大樓的地基,它決定了我們的技術高度。 我們應該多掌握一些可移值的技術或者再過十幾年應該都不會過時的技術,資料結構與演算法就是其中之一。 棧、佇列、連結串列、堆 是資料結構與演算法中的基礎知識,是程式設計師的地基。 筆者寫的 JavaScript 資料結構與演算法之美 系列用
資料結構 筆記:字串類的建立(下)
字串類中的常用成員函式 成員函式 功能描述 operator[](i) 操作符過載函式,訪問指定下標的字元 startWith(s) 判斷字串是否以s開頭 endO
資料結構與演算法之連結串列篇(下)
Q:如何輕鬆寫出正確的連結串列程式碼? 總結起來,就是投入時間+技巧; 一、投入時間: 只要願意投入時間,大多數人都是可以學會的,比如說,如果你真能花上一個週末或者一整天時間,就去寫連結
資料結構之樹與二叉樹(下)
上面兩篇我們瞭解了樹的基本概念以及二叉樹的遍歷演算法,還對二叉查詢樹進行了模擬實現。數學表示式求值是程式設計語言編譯中的一個基本問題,表示式求值是棧應用的一個典型案例,表示式分為字首、中綴和字尾三種形式。這裡,我們通過一個四則運算的應用場景,藉助二叉樹來幫助求解表
Python資料結構——二叉搜尋樹的實現(下)
搜尋樹實現(續) 最後,我們把注意力轉向二叉搜尋樹中最具挑戰性的方法,刪除一個鍵值(參見Listing 7)。首要任務是要找到搜尋樹中要刪除的節點。如果樹有一個以上的節點,我們使用_get方法找到需要刪除的節點。如果樹只有一個節點,這意味著我們要刪除樹的根,但是我們仍然要檢查根的鍵值是否與要刪除的鍵值匹配。