DTLib - LinkList、StaticLinkList

目录

线性表的链式存储结构

基本概念

单链表的具体实现与优化

单链表的遍历与优化

静态单链表的实现 

顺序表和单链表的对比分析 


 

线性表的链式存储结构

基本概念

1、基本概念

链式存储的定义

                  为了表示每个数据元素与其直接后继元素之间的逻辑关系;数 

                  据元素除了存储本身的信息外,还需要存储其直接后继的信息

链式存储逻辑结构

               -基于链式存储结构的线性表中,每个结点都包含数据域指针域 

                       数据域:存储数据元素本身

                       指针域:存储相邻结点的地址

    

专业术语的统— 

               -顺序表:基于顺序存储结构的线性表

               -链表:基于链式存储结构的线性 

                          单链表:每个结点只包含直接后继的地址信息 

                          循环链表:单链表中的最后—个结点的直接后继为第—个结点 

                          双向链表:单链表中的结点包含直接前驱和后继的地址信息 

                          双向循环链表十字链表 ......

链表中的基本概念 

               -头结点:链表中的辅助结点,包含指向第一个数据元素的指针 

               -数据结点:链表中代表数据元素的结点,表现形式为 : (数据元素,地址)

               -尾结点:链表中的最后—个数据结点,包含的地址信息为空

单链表中的结点定义

// 单链表的结点的类型
struct Node : public Object
{
    T value;    // 数据元素的具体类型
    Node* next; // 指向后继结点的指针
};

单链表中的内部结构

                      头结点在单链表中的意义是:辅助数据元素的定位,方便 

                      插入和删除操作;因此,头结点不存储实际的数据元素。 

 

单链表的具体实现与优化

1、单链表的具体实现 

LinkList设计要点 

              -类模板,通过头结点访问后继结点 

              -定义内部结点类型Node, 用于描述数据域和指针域 

              -实现线性表的关键操作(增,删,查,等)

一个关键问题:头结点是否也应该用Node定义?

头结点的隐患 :直接用Node创建头节点,头结点的数据域不会使用,若泛指类型为类类型,且类的构造函数抛出异常???所以单独定义头结点,且内存布局与Node一致

    // 结点
    struct Node : public Object
    {
        T value;
        Node* next;
    };
    // 头结点
    struct : public Object
    {
        char reserved[sizeof(T)]; // 木有实际作用,仅仅占空间,保证内存布局和Node一样
        Node* next;
    }m_header;

    // 在堆空间创建Node结点,next也会置NULL,符合单链表的特性

 

2、编程实验 

链表的实现与优化     LinkList.h

#ifndef LINKLIST_H
#define LINKLIST_H

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

namespace DTLib
{

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(int i, const T& e)
    {
        bool ret = (0 <= i) && (i <= m_length);

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

            if( node )
            {
                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 insert(const T& e)
    {
        return insert(m_length, e);
    }

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

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

            m_length--;

            delete toDel;
        }

        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;
    }

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

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

        return ret;
    }

    T get(int i) const
    {
        T ret;

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

    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;
    }

    int length() const
    {
        return m_length;
    }

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

            m_length--;

            delete toDel;
        }
    }

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

}

#endif // LINKLIST_H

main1.cpp

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

using namespace std;
using namespace DTLib;

class Test : public Object
{
public:
    Test()
    {
        throw 0;
    }
};

int main()
{
    LinkList<Test> list0;
    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) << " ";
    }

    list.remove(2);

    cout << endl;

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

    list.clear();

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

    return 0;
}

                        

main2.cpp

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

using namespace std;
using namespace DTLib;

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; // 1

    return 0;
}

 

3、小结 

            链表中的数据元素在物理内存中无相邻关系 

            链表中的结点都包含数据域和指针域 

            头结点用于辅助数据元素的定位,方便插入和删除操作 

            插入和删除操作需要保证链表的完整性 

            通过类模板实现链表,包含头结点成员长度成员 

            定义结点类型,并通过堆中的结点对象构成链式存储 

            为了避免构造错误的隐患,头结点类型需要重定义 

 线性表中元素的查找依赖于相等比较操作符(= =) 

            代码优化是编码完成后必不可少的环节 

 

单链表的遍历与优化

1、问题 

如何遍历单链表中的每—个数据元素? 

当前单链表的遍历方法

int main()
{
    LinkList<int> list;

    for(int i=0; i<5; i++)         // O(n)
    {
        list.insert(0, i);         // O(1)
    }

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

    return 0;
}

遗憾的事实:不能以线性的时间复杂度完成单链表的遍历 

新的需求:为单链表提供新的方法,在线性时间内完成遍历 

设计思路(游标) 

                -在单链表的内部定义—个游标(Node*  m_current) 

                -遍历开始前将游标指向位置为0的数据元素

                -获取游标指向的数据元素

                -通过结点中的next指针移动游标 

           提供一组遍历相关的函数,以线性的时间复杂度遍历链表
                遍历函数原型设计              功能说明
bool move(int i, int step = 1)将游标定位到目标位置
bool end()游标是否到达尾部(是否为空)
T current()获取游标所指向的数据元素
bool next()移动游标

        单链表内部的—次封装 

    // 封装create和destroy函数的意义是什么?
    virtual Node* create() 
    {
        return new Node();
    }
    virtual void destroy(Node* pn)
    {
        delete pn;
    }

 

2、编程实验 

单链表的线性遍历     LinkList.h

#ifndef LINKLIST_H
#define LINKLIST_H

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

namespace DTLib
{

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* m_current;
    int m_step;

    // 定位到要操作结点的前一个结点位置
    Node* position(int i) const
    {
        Node* ret = reinterpret_cast<Node*>(&m_header);

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

        return ret;
    }

    virtual Node* create()
    {
        return new Node();
    }

    virtual void destroy(Node* pn)
    {
        delete pn;
    }

public:
    LinkList()
    {
        m_header.next = 0;
        m_length = 0;
        m_current = 0;
        m_step = 0;
    }

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

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

            if( node )
            {
                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 insert(const T& e)
    {
        return insert(m_length, e);
    }

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

        if( ret )
        {
            Node* current = position(i);

            Node* toDel = current->next;

            if(m_current == toDel)
            {
                m_current = toDel->next;
            }

            current->next = toDel->next;

            m_length--;

            destroy(toDel);
        }

        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;
    }

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

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

        return ret;
    }

    virtual T get(int i) const
    {
        T ret;

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

    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;
    }

    int length() const
    {
        return m_length;
    }

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

            m_length--;

            destroy(toDel);
        }
    }

    virtual bool move(int i, int step = 1)
    {
        bool ret = (0 <= i) && (i < m_length) && (step > 0);

        if( ret )
        {
            m_current = position(i)->next;//游标指向第i个结点
            m_step = step;
        }

        return ret;
    }

    virtual bool end()
    {
        return m_current == 0;
    }

    virtual T current()
    {
        if( !end() )
        {
            return m_current->value;
        }
        else
        {
            THROW_EXCEPTION(InvalidParameterException, "No value at curent position ...");
        }
    }

    virtual bool next()
    {
        int i = 0;

        while((i < m_step) && !end())
        {
            m_current = m_current->next;
            i++;
        }

        return i == m_step;
    }

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

}

#endif // LINKLIST_H

main.cpp

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

using namespace std;
using namespace DTLib;


int main()
{
    LinkList<int> list;

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

    for(list.move(0); !list.end(); list.next())
    {
        cout << list.current() << " ";
    }

    cout << endl;

    for(list.move(0, 2); !list.end(); list.next())
    {
        cout << list.current() << " ";
    }

    cout << endl;

    for(list.move(0, 10); !list.end(); list.next())
    {
        cout << list.current() << endl;
    }

    return 0;
}

  

3、小结

            单链表的遍历需要在线性时间内完成 

            在单链表内部定义游标变量,通过游标变量提高效率 

            遍历相关的成员函数是相互依赖,相互配合的关系 

            封装结点的申请和删除操作更有利于增强扩展性 

静态单链表的实现 

1、静态单链表的实现 

单链表的—个缺陷 

             触发条件:长时间使用单链表对象频繁增加和删除数据元素 

             - 可能的结果:堆空间产生大量的内存碎片,导致系统运行缓慢

新的线性表 

             - 在“单链表”的内部增加—片预留的空间,所有的Node对象都在这片空间中动态创建和动态销毁。

静态单链表的继承层次结构 

            

静态单链表的实现思路 

                -通过模板定义静态单链表类(StaticLinkList) 

                -在类中定义固定大小的空间(unsigned char[]) 

                -重写create和destroy函数,改变内存的分配和归还方式 

                -在Node类中重载operator new , 用于在指定内存上创建对象

采用继承定义新结点

    typedef typename LinkList<T>::Node Node;

    struct SNode : public Node
    {
        void* operator new (unsigned int size, void* loc) // 在指定的内存上调用构造函数
        {
            (void)size; // 很多编译器里面,如果你声明了一个变量却没有使用它,编译器会发出警告,使用了就OK
            return loc; // 直接返回这片内存地址
        }
    };

父类的Node类型涉及到泛指类型,引用模板类型参数,所以需要通过LinkList<T>::Node访问

然而,编译时编译器无法确定Node是类型还是静态成员变量,所以加上typename

 

2、编程实验 

静态单链表的实现     StaticLinkList.h

#ifndef STATICLINKLIST_H
#define STATICLINKLIST_H

#include "LinkList.h"

namespace DTLib
{

template <typename T, int N>
class StaticLinkList : public LinkList<T>
{
protected:
    typedef typename LinkList<T>::Node Node;

    struct SNode : public Node
    {
        void* operator new (unsigned int size, void* loc)
        {
            (void)size;

            return loc;
        }
    };

    unsigned char m_space[sizeof(SNode) * N];
    int m_used[N];

    Node* create()
    {
        SNode* ret = 0;

        for(int i = 0; i < N; i++)
        {
            if( !m_used[i] )
            {
                ret = reinterpret_cast<SNode*>(m_space) + i;
                ret = new(ret)SNode(); // 在指定的内存调用构造函数
                m_used[i] = 1;
                break;
            }
        }

        return ret;
    }

    void destroy(Node* pn)
    {
        SNode* space = reinterpret_cast<SNode*>(m_space);
        SNode* psn = dynamic_cast<SNode*>(pn);

        for(int i = 0; i < N; i++)
        {
            if(psn == (space + i))
            {
                m_used[i] = 0;
                psn->~SNode();
                break;
            }
        }
    }

public:
    StaticLinkList()
    {
        for(int i = 0; i < N; i++)
        {
            m_used[i] = 0;
        }
    }

    int capacity()
    {
        return N;
    }

    ~StaticLinkList()
    {
        this->clear(); // this->LinkList<T>::clear();
    }
    // 析构子类对象时:先调用子类析构函数,再调用父类析构函数
    // 调用子类析构函数时,调用自己的destroy
    // 调用父类析构函数时,clear里的destroy调用父类的(实际不会调用)

};
}

#endif // STATICLINKLIST_H

main.cpp

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

using namespace std;  
using namespace DTLib;  
  
int main()  
{  
    StaticLinkList<int,5> list;  
  
    for(int i=0; i<list.capacity(); i++)  
    {  
        list.insert(0, i);  
    }  
    for(list.move(0); !list.end(); list.next())  
    {  
        cout << list.current() << endl;  
    }  
  
  
    return 0;  
}  

  

LinkList中封装create和destroy函数的意义是什么? 

为静态单链表的实现做准备。

StaticLinkList与LinkList的不同仅在于链表结点内存分配上的不同

因此,将仅有的不同封装于父类和子类的虚函数中。 

 

3、小结 

            顺序表与单链表相结合后衍生出静态单链表 

            静态单链表是LinkList的子类,拥有单链表的所有操作 

            静态单链表在预留的空间中创建结点对象 

            静态单链表适合于频繁增删数据元素的场合(最大元素个数固定)

 

顺序表和单链表的对比分析 

1、顺序表和单链表的对比分析 

时间复杂度对比分析

有趣的问题 

                            顺序表的整体时间复杂度比单链表要低,那么单链表还有使用价值吗? 

效率的深度分析 

             -实际工程开发中,时间复杂度只是效率的—个参考指标 

                     ★ 对于内置基础类型,顺序表和单链表的效率不相上下 

                     ★ 对于自定义类类型顺序表在效率上低于单链表 

             -插入和删除 

                     ★ 顺序表:涉及大量数据对象的复制操作 

                     ★ 单链表:只涉及指针操作,效率与数据对象无关 

             -数据访问 

                     ★ 顺序表:随机访问,可直接定位数据对象 

                     ★ 单链表:顺序访问,必须从头访问数据对象,无法直接定位

 

工程开发中的选择 

             -顺序表 

                     ★ 数据元素的类型相对简单,不涉及深拷贝 

                     ★ 数据元素相对稳定,访问操作远多于插入和删除操作 

             单链表 

                     ★ 数据元素的类型相对复杂,复制操作相对耗时 

                     ★ 数据元素不稳定,需要经常插入和删除,访问操作较少 


2、小结 

            顺序表适用于访问需求量较大的场合(随机访问) 

            单链表适用于数据元素频繁插入删除的场合(顺序访问) 

            当数据类型相对简单时,顺序表和单链表的效率不相上下 

 

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值