數據結構基礎溫故-1.線性表(中)

上一篇中,咱們學習了線性表最基礎的表現形式-順序表,可是其存在必定缺點:必須佔用一整塊事先分配好的存儲空間,在插入和刪除操做上須要移動大量元素(即操做不方便),因而不受固定存儲空間限制而且能夠進行比較快捷地插入和刪除操做的鏈表橫空出世,因此咱們就來複習一下鏈表。html

1、單鏈表基礎

1.1 單鏈表的節點結構

  在鏈表中,每一個節點由兩部分組成:數據域指針域node

1.2 單鏈表的整體結構

  鏈表就是由N個節點連接而成的線性表,若是其中每一個節點只包含一個指針域那麼就稱爲單鏈表,若是含有兩個指針域那麼就稱爲雙鏈表數據結構

PS:在線性表的鏈式存儲結構中,爲了便於插入和刪除操做的實現,每一個鏈表都帶有一個頭指針(或尾指針),經過頭指針能夠惟一標識該鏈表。從頭指針所指向的節點出發,沿着節點的鏈能夠訪問到每一個節點。ide

2、單鏈表實現

2.1 單鏈表節點的定義

    public class Node<T>
    {
        // 數據域
        public T Item { get; set; }
        // 指針域
        public Node<T> Next { get; set; }

        public Node()
        {
        }

        public Node(T item)
        {
            this.Item = item;
        }
    }

  此處定義Node類爲單鏈表的節點,其中包括了一個數據域Item與一個指針域Next(指向後繼節點的位置)。學習

2.2 單鏈表節點的新增

  ①默認在尾節點後插入新節點測試

        public void Add(T value)
        {
            Node<T> newNode = new Node<T>(value);
            if (this.head == null)
            {
                // 若是鏈表當前爲空則置爲頭結點
                this.head = newNode;
            }
            else
            {
                Node<T> prevNode = this.GetNodeByIndex(this.count - 1);
                prevNode.Next = newNode;
            }

            this.count++;
        }    

  首先判斷頭結點是否爲空,其次依次遍歷各節點找到尾節點的前驅節點,而後更改前驅節點的Next指針指向新節點便可。this

  ②指定在某個節點後插入新節點spa

        public void Insert(int index, T value)
        {
            Node<T> tempNode = null;
            if (index < 0 || index > this.count)
            {
                throw new ArgumentOutOfRangeException("index", "索引超出範圍");
            }
            else if (index == 0)
            {
                if (this.head == null)
                {
                    tempNode = new Node<T>(value);
                    this.head = tempNode;
                }
                else
                {
                    tempNode = new Node<T>(value);
                    tempNode.Next = this.head;
                    this.head = tempNode;
                }
            }
            else
            {
                Node<T> prevNode = GetNodeByIndex(index - 1);
                tempNode = new Node<T>(value);
                tempNode.Next = prevNode.Next;
                prevNode.Next = tempNode;
            }

            this.count++;
        }

  這裏須要判斷是不是在第一個節點進行插入,若是是則再次判斷頭結點是否爲空。3d

2.3 單鏈表節點的移除

        public void RemoveAt(int index)
        {
            if (index == 0)
            {
                this.head = this.head.Next;
            }
            else
            {
                Node<T> prevNode = GetNodeByIndex(index - 1);
                if (prevNode.Next == null)
                {
                    throw new ArgumentOutOfRangeException("index", "索引超出範圍");
                }

                Node<T> deleteNode = prevNode.Next;
                prevNode.Next = deleteNode.Next;

                deleteNode = null;
            }

            this.count--;
        }

  移除某個節點只需將其前驅節點的Next指針指向要移除節點的後繼節點便可。指針

  至此,關鍵部分的代碼已介紹完畢,下面給出完整的單鏈表模擬實現代碼:

/// <summary>
/// 單鏈表模擬實現
/// </summary>
public class MySingleLinkedList<T>
{
        private int count; // 字段:當前鏈表節點個數
        private Node<T> head; // 字段:當前鏈表的頭結點

        // 屬性:當前鏈表節點個數
        public int Count
        {
            get
            {
                return this.count;
            }
        }

        // 索引器
        public T this[int index]
        {
            get
            {
                return this.GetNodeByIndex(index).Item;
            }
            set
            {
                this.GetNodeByIndex(index).Item = value;
            }
        }

        public MySingleLinkedList()
        {
            this.count = 0;
            this.head = null;
        }

        // Method01:根據索引獲取節點
        private Node<T> GetNodeByIndex(int index)
        {
            if (index < 0 || index >= this.count)
            {
                throw new ArgumentOutOfRangeException("index", "索引超出範圍");
            }

            Node<T> tempNode = this.head;
            for (int i = 0; i < index; i++)
            {
                tempNode = tempNode.Next;
            }

            return tempNode;
        }

        // Method02:在尾節點後插入新節點
        public void Add(T value)
        {
            Node<T> newNode = new Node<T>(value);
            if (this.head == null)
            {
                // 若是鏈表當前爲空則置爲頭結點
                this.head = newNode;
            }
            else
            {
                Node<T> prevNode = this.GetNodeByIndex(this.count - 1);
                prevNode.Next = newNode;
            }

            this.count++;
        }

        // Method03:在指定位置插入新節點
        public void Insert(int index, T value)
        {
            Node<T> tempNode = null;
            if (index < 0 || index > this.count)
            {
                throw new ArgumentOutOfRangeException("index", "索引超出範圍");
            }
            else if (index == 0)
            {
                if (this.head == null)
                {
                    tempNode = new Node<T>(value);
                    this.head = tempNode;
                }
                else
                {
                    tempNode = new Node<T>(value);
                    tempNode.Next = this.head;
                    this.head = tempNode;
                }
            }
            else
            {
                Node<T> prevNode = GetNodeByIndex(index - 1);
                tempNode = new Node<T>(value);
                tempNode.Next = prevNode.Next;
                prevNode.Next = tempNode;
            }

            this.count++;
        }

        // Method04:移除指定位置的節點
        public void RemoveAt(int index)
        {
            if (index == 0)
            {
                this.head = this.head.Next;
            }
            else
            {
                Node<T> prevNode = GetNodeByIndex(index - 1);
                if (prevNode.Next == null)
                {
                    throw new ArgumentOutOfRangeException("index", "索引超出範圍");
                }

                Node<T> deleteNode = prevNode.Next;
                prevNode.Next = deleteNode.Next;

                deleteNode = null;
            }

            this.count--;
        }
    }
}
View Code

2.4 單鏈表的模擬實現簡單測試

  這裏針對模擬的單鏈表進行三個簡單的測試:一是順序插入4個節點;二是在指定的位置插入單個節點;三是移除指定位置的單個節點;測試代碼以下所示:

    static void MySingleLinkedListTest()
    {
            MySingleLinkedList<int> linkedList = new MySingleLinkedList<int>();
            // Test1:順序插入4個節點
            linkedList.Add(0);
            linkedList.Add(1);
            linkedList.Add(2);
            linkedList.Add(3);

            Console.WriteLine("The nodes in the linkedList:");
            for (int i = 0; i < linkedList.Count; i++)
            {
                Console.WriteLine(linkedList[i]);
            }
            Console.WriteLine("----------------------------");

            // Test2.1:在索引爲0(即第1個節點)的位置插入單個節點
            linkedList.Insert(0, 10);
            Console.WriteLine("After insert 10 in index of 0:");
            for (int i = 0; i < linkedList.Count; i++)
            {
                Console.WriteLine(linkedList[i]);
            }
            // Test2.2:在索引爲2(即第3個節點)的位置插入單個節點
            linkedList.Insert(2, 20);
            Console.WriteLine("After insert 20 in index of 2:");
            for (int i = 0; i < linkedList.Count; i++)
            {
                Console.WriteLine(linkedList[i]);
            }
            // Test2.3:在索引爲5(即最後一個節點)的位置插入單個節點
            linkedList.Insert(5, 30);
            Console.WriteLine("After insert 30 in index of 5:");
            for (int i = 0; i < linkedList.Count; i++)
            {
                Console.WriteLine(linkedList[i]);
            }
            Console.WriteLine("----------------------------");

            // Test3.1:移除索引爲5(即最後一個節點)的節點
            linkedList.RemoveAt(5);
            Console.WriteLine("After remove an node in index of 5:");
            for (int i = 0; i < linkedList.Count; i++)
            {
                Console.WriteLine(linkedList[i]);
            }
            // Test3.2:移除索引爲0(即第一個節點)的節點
            linkedList.RemoveAt(0);
            Console.WriteLine("After remove an node in index of 0:");
            for (int i = 0; i < linkedList.Count; i++)
            {
                Console.WriteLine(linkedList[i]);
            }
            // Test3.3:移除索引爲2(即第三個節點)的節點
            linkedList.RemoveAt(2);
            Console.WriteLine("After remove an node in index of 2:");
            for (int i = 0; i < linkedList.Count; i++)
            {
                Console.WriteLine(linkedList[i]);
            }
            Console.WriteLine("----------------------------");
        } 
        #endregion
    }

  測試結果以下圖所示:

  ①順序插入4個新節點

  ②在指定位置插入新節點

  ③在指定位置移除某個節點

3、雙鏈表基礎

3.1 雙鏈表的節點結構

  與單鏈表不一樣的是,雙鏈表有兩個指針域,一個指向前驅節點,另外一個指向後繼節點。

3.2 雙鏈表的整體結構

  雙鏈表中,每一個節點都有兩個指針,指向前驅和後繼,這樣能夠方便地找到某個節點的前驅節點和後繼節點,這在某些場合中是很是實用的。

4、雙鏈表實現

4.1 雙鏈表節點的定義

    public class DbNode<T>
    {
        public T Item { get; set; }
        public DbNode<T> Prev { get; set; }
        public DbNode<T> Next { get; set; }

        public DbNode()
        {
        }

        public DbNode(T item)
        {
            this.Item = item;
        }
    }

  與單鏈表的節點定義不一樣的是,多了一個指向前驅節點的Prev指針域,能夠方便地找到某個節點的前驅節點,從而沒必要從新遍歷一次。

4.2 雙鏈表中插入新節點

  ①默認在尾節點以後插入新節點

        public void AddAfter(T value)
        {
            DbNode<T> newNode = new DbNode<T>(value);
            if (this.head == null)
            {
                // 若是鏈表當前爲空則置爲頭結點
                this.head = newNode;
            }
            else
            {
                DbNode<T> lastNode = this.GetNodeByIndex(this.count - 1);
                // 調整插入節點與前驅節點指針關係
                lastNode.Next = newNode;
                newNode.Prev = lastNode;
            }
            this.count++;
        }

  ②可選在尾節點以前插入新節點

        public void AddBefore(T value)
        {
            DbNode<T> newNode = new DbNode<T>(value);
            if (this.head == null)
            {
                // 若是鏈表當前爲空則置爲頭結點
                this.head = newNode;
            }
            else
            {
                DbNode<T> lastNode = this.GetNodeByIndex(this.count - 1);
                DbNode<T> prevNode = lastNode.Prev;
                // 調整倒數第2個節點與插入節點的關係
                prevNode.Next = newNode;
                newNode.Prev = prevNode;
                // 調整倒數第1個節點與插入節點的關係
                lastNode.Prev = newNode;
                newNode.Next = lastNode;
            }
            this.count++;
        }

  典型的四個步驟,調整插入節點與尾節點前驅節點的關係、插入節點與尾節點的關係。

  固然,還能夠在指定的位置以前或以後插入新節點,例如InsertAfter和InsertBefore方法,代碼詳見下面4.3後面的完整實現。

4.3 雙鏈表中移除某個節點

        public void RemoveAt(int index)
        {
            if (index == 0)
            {
                this.head = this.head.Next;
            }
            else
            {
                DbNode<T> prevNode = this.GetNodeByIndex(index - 1);
                if (prevNode.Next == null)
                {
                    throw new ArgumentOutOfRangeException("index", "索引超出範圍");
                }

                DbNode<T> deleteNode = prevNode.Next;
                DbNode<T> nextNode = deleteNode.Next;
                prevNode.Next = nextNode;
                if(nextNode != null)
                {
                    nextNode.Prev = prevNode;
                }

                deleteNode = null;
            }
            this.count--;
        }

  這裏只須要將前驅節點的Next指針指向待刪除節點的後繼節點,將後繼節點的Prev指針指向待刪除節點的前驅節點便可。

  至此,關鍵部分的代碼已介紹完畢,下面給出完整的雙鏈表模擬實現代碼:

    /// <summary>
    /// 雙鏈表的模擬實現
    /// </summary>
    public class MyDoubleLinkedList<T>
    {
        private int count; // 字段:當前鏈表節點個數
        private DbNode<T> head; // 字段:當前鏈表的頭結點

        // 屬性:當前鏈表節點個數
        public int Count
        {
            get
            {
                return this.count;
            }
        }

        // 索引器
        public T this[int index]
        {
            get
            {
                return this.GetNodeByIndex(index).Item;
            }
            set
            {
                this.GetNodeByIndex(index).Item = value;
            }
        }

        public MyDoubleLinkedList()
        {
            this.count = 0;
            this.head = null;
        }

        // Method01:根據索引獲取節點
        private DbNode<T> GetNodeByIndex(int index)
        {
            if (index < 0 || index >= this.count)
            {
                throw new ArgumentOutOfRangeException("index", "索引超出範圍");
            }

            DbNode<T> tempNode = this.head;
            for (int i = 0; i < index; i++)
            {
                tempNode = tempNode.Next;
            }

            return tempNode;
        }

        // Method02:在尾節點後插入新節點
        public void AddAfter(T value)
        {
            DbNode<T> newNode = new DbNode<T>(value);
            if (this.head == null)
            {
                // 若是鏈表當前爲空則置爲頭結點
                this.head = newNode;
            }
            else
            {
                DbNode<T> lastNode = this.GetNodeByIndex(this.count - 1);
                // 調整插入節點與前驅節點指針關係
                lastNode.Next = newNode;
                newNode.Prev = lastNode;
            }
            this.count++;
        }

        // Method03:在尾節點前插入新節點
        public void AddBefore(T value)
        {
            DbNode<T> newNode = new DbNode<T>(value);
            if (this.head == null)
            {
                // 若是鏈表當前爲空則置爲頭結點
                this.head = newNode;
            }
            else
            {
                DbNode<T> lastNode = this.GetNodeByIndex(this.count - 1);
                DbNode<T> prevNode = lastNode.Prev;
                // 調整倒數第2個節點與插入節點的關係
                prevNode.Next = newNode;
                newNode.Prev = prevNode;
                // 調整倒數第1個節點與插入節點的關係
                lastNode.Prev = newNode;
                newNode.Next = lastNode;
            }
            this.count++;
        }

        // Method04:在指定位置後插入新節點
        public void InsertAfter(int index, T value)
        {
            DbNode<T> tempNode;
            if (index == 0)
            {
                if (this.head == null)
                {
                    tempNode = new DbNode<T>(value);
                    this.head = tempNode;
                }
                else
                {
                    tempNode = new DbNode<T>(value);
                    tempNode.Next = this.head;
                    this.head.Prev = tempNode;
                    this.head = tempNode;
                }
            }
            else
            {
                DbNode<T> prevNode = this.GetNodeByIndex(index); // 得到插入位置的節點
                DbNode<T> nextNode = prevNode.Next; // 獲取插入位置的後繼節點
                tempNode = new DbNode<T>(value);
                // 調整插入節點與前驅節點指針關係
                prevNode.Next = tempNode;
                tempNode.Prev = prevNode;
                // 調整插入節點與後繼節點指針關係
                if (nextNode != null)
                {
                    tempNode.Next = nextNode;
                    nextNode.Prev = tempNode;
                }
            }
            this.count++;
        }

        // Method05:在指定位置前插入新節點
        public void InsertBefore(int index, T value)
        {
            DbNode<T> tempNode;
            if (index == 0)
            {
                if (this.head == null)
                {
                    tempNode = new DbNode<T>(value);
                    this.head = tempNode;
                }
                else
                {
                    tempNode = new DbNode<T>(value);
                    tempNode.Next = this.head;
                    this.head.Prev = tempNode;
                    this.head = tempNode;
                }
            }
            else
            {
                DbNode<T> nextNode = this.GetNodeByIndex(index); // 得到插入位置的節點
                DbNode<T> prevNode = nextNode.Prev; // 獲取插入位置的前驅節點
                tempNode = new DbNode<T>(value);
                // 調整插入節點與前驅節點指針關係
                prevNode.Next = tempNode;
                tempNode.Prev = prevNode;
                // 調整插入節點與後繼節點指針關係
                tempNode.Next = nextNode;
                nextNode.Prev = tempNode;
            }
            this.count++;
        }

        // Method06:移除指定位置的節點
        public void RemoveAt(int index)
        {
            if (index == 0)
            {
                this.head = this.head.Next;
            }
            else
            {
                DbNode<T> prevNode = this.GetNodeByIndex(index - 1);
                if (prevNode.Next == null)
                {
                    throw new ArgumentOutOfRangeException("index", "索引超出範圍");
                }

                DbNode<T> deleteNode = prevNode.Next;
                DbNode<T> nextNode = deleteNode.Next;
                prevNode.Next = nextNode;
                if(nextNode != null)
                {
                    nextNode.Prev = prevNode;
                }

                deleteNode = null;
            }
            this.count--;
        }
    }
View Code

4.4 雙鏈表模擬實現的簡單測試

  這裏跟單鏈表同樣,進行幾個簡單的測試:一是順序插入(默認在尾節點以後)4個新節點,二是在尾節點以前和在指定索引位置插入新節點,三是移除指定索引位置的節點,四是修改某個節點的Item值。測試代碼以下所示。

        static void MyDoubleLinkedListTest()
        {
            MyDoubleLinkedList<int> linkedList = new MyDoubleLinkedList<int>();
            // Test1:順序插入4個節點
            linkedList.AddAfter(0);
            linkedList.AddAfter(1);
            linkedList.AddAfter(2);
            linkedList.AddAfter(3);

            Console.WriteLine("The nodes in the DoubleLinkedList:");
            for (int i = 0; i < linkedList.Count; i++)
            {
                Console.Write(linkedList[i] + " ");
            }
            Console.WriteLine();
            Console.WriteLine("----------------------------");
            // Test2.1:在尾節點以前插入2個節點
            linkedList.AddBefore(10);
            linkedList.AddBefore(20);
            Console.WriteLine("After add 10 and 20:");
            for (int i = 0; i < linkedList.Count; i++)
            {
                Console.Write(linkedList[i] + " ");
            }
            Console.WriteLine();
            // Test2.2:在索引爲2(即第3個節點)的位置以後插入單個節點
            linkedList.InsertAfter(2, 50);
            Console.WriteLine("After add 50:");
            for (int i = 0; i < linkedList.Count; i++)
            {
                Console.Write(linkedList[i] + " ");
            }
            Console.WriteLine();
            // Test2.3:在索引爲2(即第3個節點)的位置以前插入單個節點
            linkedList.InsertBefore(2, 40);
            Console.WriteLine("After add 40:");
            for (int i = 0; i < linkedList.Count; i++)
            {
                Console.Write(linkedList[i] + " ");
            }
            Console.WriteLine();
            Console.WriteLine("----------------------------");
            // Test3.1:移除索引爲7(即最後一個節點)的位置的節點
            linkedList.RemoveAt(7);
            Console.WriteLine("After remove an node in index of 7:");
            for (int i = 0; i < linkedList.Count; i++)
            {
                Console.Write(linkedList[i] + " ");
            }
            Console.WriteLine();
            // Test3.2:移除索引爲0(即第一個節點)的位置的節點的值
            linkedList.RemoveAt(0);
            Console.WriteLine("After remove an node in index of 0:");
            for (int i = 0; i < linkedList.Count; i++)
            {
                Console.Write(linkedList[i] + " ");
            }
            Console.WriteLine();
            // Test3.3:移除索引爲2(即第3個節點)的位置的節點
            linkedList.RemoveAt(2);
            Console.WriteLine("After remove an node in index of 2:");
            for (int i = 0; i < linkedList.Count; i++)
            {
                Console.Write(linkedList[i] + " ");
            }
            Console.WriteLine();
            Console.WriteLine("----------------------------");
            // Test4:修改索引爲2(即第3個節點)的位置的節點的值
            linkedList[2] = 9;
            Console.WriteLine("After update the value of node in index of 2:");
            for (int i = 0; i < linkedList.Count; i++)
            {
                Console.Write(linkedList[i] + " ");
            }
            Console.WriteLine();
            Console.WriteLine("----------------------------");
        }

  測試結果以下圖所示。

5、.NET中的ListDictionary與LinkedList<T>

  在.NET中,已經爲咱們提供了單鏈表和雙鏈表的實現,它們分別是ListDictionary與LinkedList<T>。從名稱能夠看出,單鏈表的實現ListDictionary不是泛型實現,而LinkedList是泛型實現,它們又到底有什麼區別呢,藉助Reflector去看看吧。

5.1 ListDictionary—基於key/value的單鏈表

  ListDictionary位於System.Collection.Specialized下,它是基於鍵值對(Key/Value)的集合,微軟給出的建議是:一般用於包含10個或10個如下項的集合。

  它的節點的數據域是一個鍵值對,而不是一個簡單的value。

5.2 LinkedList—神奇的泛型雙向鏈表

  在.NET中,LinkedList<T>是使用地比較多的鏈表實現類,它位於System.Collections.Generic下,是一個通用的雙向鏈表類,它不支持隨機訪問(即索引訪問),但它實現了不少的新增節點的方法,例如:AddAfter、AddBefore、AddFirst以及AddLast等。其中,AddFirst是在現有節點以後添加新節點,AddBefore則是在現有節點以前添加新節點,AddFirst是在開頭處添加,而AddLast則是在末尾處添加。

參考資料

(1)程傑,《大話數據結構》

(2)陳廣,《數據結構(C#語言描述)》

(3)段恩澤,《數據結構(C#語言版)》

(4)率輝,《數據結構高分筆記(2015版)》

 

相關文章
相關標籤/搜索