1. 程式人生 > >數據結構開發(5):線性表的鏈式存儲結構

數據結構開發(5):線性表的鏈式存儲結構

插入 設計要點 res def cast 解決 數據結構 get move

0.目錄

1.線性表的鏈式存儲結構

2.單鏈表的具體實現

3.順序表和單鏈表的對比分析

4.小結

1.線性表的鏈式存儲結構

順序存儲結構線性表的最大問題是:

  • 插入和刪除需要移動大量的元素!如何解決?

鏈式存儲的定義:

  • 為了表示每個數據元素與其直接後繼元素之間的邏輯關系;數據元素除了存儲本身的信息外,還需要存儲其直接後繼的信息。

技術分享圖片

鏈式存儲邏輯結構:

  • 基於鏈式存儲結構的線性表中,每個結點都包含數據域指針域
    1. 數據域:存儲數據元素本身
    2. 指針域:存儲相鄰結點的地址

技術分享圖片

專業術語的統一:

  • 順序表
    1. 基於順序存儲結構的線性表
  • 鏈表
    1. 基於鏈式存儲機構的線性表
      1. 單鏈表:每個結點只包含直接後繼的地址信息
      2. 循環鏈表:單鏈表中的最後一個結點的直接後繼為第一個結點
      3. 雙向鏈表:單鏈表中的結點包含直接前驅和後繼的地址信息

鏈表中的基本概念:

  • 頭結點
    1. 鏈表中的輔助結點,包含指向第一個數據元素的指針
  • 數據結點
    1. 鏈表中代表數據元素的結點,表現形式為:( 數據元素,地址 )
  • 尾結點
    1. 鏈表中的最後一個數據結點,包含的地址信息為空

單鏈表中的結點定義:
技術分享圖片

單鏈表中的內部結構:
技術分享圖片
頭結點在單鏈表中的意義是:輔助數據元素的定位,方便插入和刪除操作;因此,頭結點不存儲實際的數據元素

在目標位置處插入數據元素:

  1. 從頭結點開始,通過current指針定位到目標位置
  2. 從堆空間申請新的Node結點
  3. 執行操作:
    1. node->value = e;
    2. node->next = current->next;
    3. current->next = node;

在目標位置處刪除數據元素:

  1. 從頭結點開始,通過current指針定位到目標位置
  2. 使用toDel指針指向需要刪除的結點
  3. 執行操作:
    1. toDel = current->next;
    2. current->next = toDel->next;
    3. delete toDel;

2.單鏈表的具體實現

本節目標:

  • 完成鏈式存儲結構線性表的實現

技術分享圖片

LinkList 設計要點:

  1. 類模板,通過頭結點訪問後繼結點
  2. 定義內部結點類型Node,用於描述數據域和指針域
  3. 實現線性表的關鍵操作
    ( 增,刪,查,等 )

LinkList的定義:
技術分享圖片

鏈表的實現 LinkList.h:

#ifndef LINKLIST_H
#define LINKLIST_H

#include "List.h"
#include "Exception.h"

namespace StLib
{

template <typename T>
class LinkList : public List<T>
{
protected:
    struct Node : public Object
    {
        T value;
        Node* next;
    };

    mutable Node m_header;
    int m_length;
public:
    LinkList()
    {
        m_header.next = NULL;
        m_length = 0;
    }

    bool insert(const T& e)
    {
        return insert(m_length, e);
    }

    bool insert(int i, const T& e)
    {
        bool ret = ((0 <= i) && (i <= m_length));

        if( ret )
        {
            Node* node = new Node();

            if( node != NULL )
            {
                Node* current = &m_header;

                for(int p=0; p<i; p++)
                {
                    current = current->next;
                }

                node->value = e;
                node->next = current->next;
                current->next = node;

                m_length++;
            }
            else
            {
                THROW_EXCEPTION(NoEnoughMemoryException, "No memory to insert new element ...");
            }
        }

        return ret;
    }

    bool remove(int i)
    {
        bool ret = ((0 <= i) && (i < m_length));

        if( ret )
        {
            Node* current = &m_header;

            for(int p=0; p<i; p++)
            {
                current = current->next;
            }

            Node* toDel = current->next;

            current->next = toDel->next;

            delete toDel;

            m_length--;
        }

        return ret;
    }

    bool set(int i, const T& e)
    {
        bool ret = ((0 <= i) && (i < m_length));

        if( ret )
        {
            Node* current = &m_header;

            for(int p=0; p<i; p++)
            {
                current = current->next;
            }

            current->next->value = e;
        }

        return ret;
    }

    T get(int i) const
    {
        T ret;

        if( get(i, ret) )
        {
            return ret;
        }
        else
        {
            THROW_EXCEPTION(IndexOutOfBoundsException, "Invalid parameter i to get element ...");
        }

        return ret;
    }

    bool get(int i, T& e) const
    {
        bool ret = ((0 <= i) && (i < m_length));

        if( ret )
        {
            Node* current = &m_header;

            for(int p=0; p<i; p++)
            {
                current = current->next;
            }

            e = current->next->value;
        }

        return ret;
    }

    int length() const
    {
        return m_length;
    }

    void clear()
    {
        while ( m_header.next )
        {
            Node* toDel = m_header.next;

            m_header.next = toDel->next;

            delete toDel;
        }

        m_length = 0;
    }

    ~LinkList()
    {
        clear();
    }
};

}

#endif // LINKLIST_H

main.cpp測試

#include <iostream>
#include "LinkList.h"

using namespace std;
using namespace StLib;

int main()
{
    LinkList<int> list;

    for(int i=0; i<5; i++)
    {
        list.insert(0, i);
        list.set(0, i*i);
    }

    for(int i=0; i<list.length(); i++)
    {
        cout << list.get(i) << endl;
    }
    cout << endl;

    list.remove(2);

    for(int i=0; i<list.length(); i++)
    {
        cout << list.get(i) << endl;
    }
    cout << endl;

    list.clear();

    for(int i=0; i<list.length(); i++)
    {
        cout << list.get(i) << endl;
    }

    return 0;
}

運行結果為:

16
9
4
1
0

16
9
1
0

問題:

  • 頭結點是否存在隱患?
  • 實現代碼是否需要優化?

頭結點的隱患:
技術分享圖片

代碼優化:

  • insert,remove,get,set等操作都涉及元素定位。

技術分享圖片

代碼優化(LinkList.h):

#ifndef LINKLIST_H
#define LINKLIST_H

#include "List.h"
#include "Exception.h"

namespace StLib
{

template <typename T>
class LinkList : public List<T>
{
protected:
    struct Node : public Object
    {
        T value;
        Node* next;
    };

    mutable struct : public Object
    {
        char reserved[sizeof(T)];
        Node* next;
    } m_header;

    int m_length;

    Node* position(int i) const
    {
        Node* ret = reinterpret_cast<Node*>(&m_header);

        for(int p=0; p<i; p++)
        {
            ret = ret->next;
        }

        return ret;
    }
public:
    LinkList()
    {
        m_header.next = NULL;
        m_length = 0;
    }

    bool insert(const T& e)
    {
        return insert(m_length, e);
    }

    bool insert(int i, const T& e)
    {
        bool ret = ((0 <= i) && (i <= m_length));

        if( ret )
        {
            Node* node = new Node();

            if( node != NULL )
            {
                Node* current = position(i);

                node->value = e;
                node->next = current->next;
                current->next = node;

                m_length++;
            }
            else
            {
                THROW_EXCEPTION(NoEnoughMemoryException, "No memory to insert new element ...");
            }
        }

        return ret;
    }

    bool remove(int i)
    {
        bool ret = ((0 <= i) && (i < m_length));

        if( ret )
        {
            Node* current = position(i);
            Node* toDel = current->next;

            current->next = toDel->next;

            delete toDel;

            m_length--;
        }

        return ret;
    }

    bool set(int i, const T& e)
    {
        bool ret = ((0 <= i) && (i < m_length));

        if( ret )
        {
            position(i)->next->value = e;
        }

        return ret;
    }

    T get(int i) const
    {
        T ret;

        if( get(i, ret) )
        {
            return ret;
        }
        else
        {
            THROW_EXCEPTION(IndexOutOfBoundsException, "Invalid parameter i to get element ...");
        }

        return ret;
    }

    bool get(int i, T& e) const
    {
        bool ret = ((0 <= i) && (i < m_length));

        if( ret )
        {
            e = position(i)->next->value;
        }

        return ret;
    }

    int length() const
    {
        return m_length;
    }

    void clear()
    {
        while ( m_header.next )
        {
            Node* toDel = m_header.next;

            m_header.next = toDel->next;

            delete toDel;
        }

        m_length = 0;
    }

    ~LinkList()
    {
        clear();
    }
};

}

#endif // LINKLIST_H

3.順序表和單鏈表的對比分析

問題

  • 如何判斷某個數據元素是否存在線性表中?

遺失的操作——find

  • 可以為線性表( List )增加一個查找操作
  • int find(const T& e) const;
    1. 參數:
      1. 待查找的數據元素
    2. 返回值:
      1. =0:數據元素在線性表中第一次出現的位置

      2. -1:數據元素不存在

數據元素查找示例:
技術分享圖片

實現查找find函數:
在List.h中加入

virtual int find(const T& e) const = 0;

在SeqList.h中加入

    int find(const T& e) const
    {
        int ret = -1;

        for(int i=0; i<m_length; i++)
        {
            if( m_array[i] == e )
            {
                ret = i;
                break;
            }
        }

        return ret;
    }

在LinkList.h中加入

    int find(const T& e) const
    {
        int ret = -1;
        int i = 0;
        Node* node = m_header.next;

        while ( node )
        {
            if( node->value == e )
            {
                ret = i;
                break;
            }
            else
            {
                node = node->next;
                i++;
            }
        }

        return ret;
    }

但是若用類對象來進行測試,會有嚴重的bug:

#include <iostream>
#include "LinkList.h"

using namespace std;
using namespace StLib;

class Test
{
    int i;
public:
    Test(int v = 0)
    {
        i = v;
    }
};

int main()
{
    Test t1;
    Test t2;
    Test t3;
    LinkList<Test> list;

    return 0;
}

編譯錯誤信息:

error C2678: 二進制“==”: 沒有找到接受“Test”類型的左操作數的運算符(或沒有可接受的轉換)

於是應該在頂層父類Object中實現重載比較操作符
Object.h

    bool operator == (const Object& obj);
    bool operator != (const Object& obj);

Object.cpp

bool Object::operator == (const Object& obj)
{
    return (this == &obj);
}

bool Object::operator != (const Object& obj)
{
    return (this != &obj);
}

main.cpp再測試

#include <iostream>
#include "LinkList.h"

using namespace std;
using namespace StLib;

class Test : public Object
{
    int i;
public:
    Test(int v = 0)
    {
        i = v;
    }

    bool operator == (const Test& t)
    {
        return (i == t.i);
    }
};

int main()
{
    Test t1(1);
    Test t2(2);
    Test t3(3);
    LinkList<Test> list;

    list.insert(t1);
    list.insert(t2);
    list.insert(t3);

    cout << list.find(t2) << endl;

    return 0;
}

運行結果為:

1

時間復雜度對比分析:
技術分享圖片

有趣的問題:

  • 順序表的整體時間復雜度比單鏈表要低那麽單鏈表還有使用價值嗎?

效率的深度分析:

  • 實際工程開發中,時間復雜度只是效率的一個參考指標
    1. 對於內置基礎類型,順序表和單鏈表的下效率不相上
    2. 對於自定義類類型順序表在效率上低於單鏈表
  • 插入和刪除
    1. 順序表:涉及大量數據對象的復制操作
    2. 單鏈表:只涉及指針操作,效率與數據對象無關
  • 數據訪問
    1. 順序表:隨機訪問,可直接定位數據對象
    2. 單鏈表:順序訪問,必須從頭訪問數據對象,無法直接定位

工程開發中的選擇:

  • 順序表
    1. 數據元素的類型相對簡單,不涉及深拷貝
    2. 數據元素相對穩定,訪問操作遠多於插入和刪除操作
  • 單鏈表
    1. 數據元素的類型相對復雜,復制操作相對耗時
    2. 數據元素不穩定,需要經常插入和刪除,訪問操作較少

4.小結

  • 鏈表中的數據元素在物理內存中無相鄰關系
  • 鏈表中的結點都包含數據域指針域
  • 頭結點用於輔助數據元素的定位,方便插入和刪除操作
  • 插入和刪除操作需要保證鏈表的完整性
  • 通過類模板實現鏈表,包含頭結點成員長度成員
  • 定義結點類型,並通過堆中的結點對象構成鏈式存儲
  • 為了避免構造錯誤的隱患,頭結點類型需要重定義
  • 代碼優化是編碼完成後必不可少的環節
  • 線性表中元素的查找依賴於相等比較操作符( == )
  • 順序表適用於訪問需求量較大的場合( 隨機訪問 )
  • 單鏈表適用於數據元素頻繁插入刪除的場合( 順序訪問 )
  • 當數據類型相對簡單時,順序表和單鏈表的效率不相上下

數據結構開發(5):線性表的鏈式存儲結構