有哨兵的双向循环链表、单向循环链表

  • 思路分析

    对于普通的双向循环链表,因为同其他节点相比,头尾元素是特别的,需要特别处理,所以在插入和删除节点的时候需要判断边界条件,这会让代码显得臃肿。头节点的特殊之处在于它的前驱是空指针,尾节点的特殊之处在于它的后继节点是空指针。

    如果能令头尾元素不再特别,就可以省去边界条件的处理了。那么问题来了,如何让他们不特殊呢?那就需要让每个元素的前驱和后继都不为空。自然会想到在头元素之前,尾元素之后分别添加一个无意义的节点,使得他们的前驱和后继不空。

    如此一来,一个空的链表从初始化就有了两个无意义的节点。然后你会发现,这里有了数据冗余,两个无意义的节点其实可以省略掉一个,让头尾节点都与同一个哨兵节点相连,到此,就形成了一个有哨兵的双向循环链表。哨兵的作用是可以降低运行时间中的常数因子。

    当链表为空的时候,哨兵的前驱和后继都是其自身。(以上内容总结自《算法导论》10.2例题)下面附上一个简单的C++实现。

struct Node
{
    Node* pPrev;    //前驱节点
    Node* pNext;    //后继节点
    int   key;        //当前节点的键值
};

class DoubleLinkedList
{
public:
    DoubleLinkedList();
    ~DoubleLinkedList();

    Node* Search(int key);    //查找键值为key的节点,若没有找到返回空指针
    void Insert(int key);                    //在头部插入一个键值为key的节点
    void Delete(Node* p);                    //删除节点p
private:
    Node m_sentinel;
};

DoubleLinkedList::DoubleLinkedList()
{
    m_sentinel.pPrev = &m_sentinel;    //初始化时链表为空,哨兵的前驱和后继都指向其自身。
    m_sentinel.pNext = &m_sentinel;
}

DoubleLinkedList::~DoubleLinkedList()
{
    Node* pCurrent = m_sentinel.pNext;
    Node* pNext = nullptr;
    while(pCurrent != &m_sentinel)
    {
        pNext = pCurrent->pNext;
        delete pCurrent;    //循环的每一步负责删除当前节点,并将pCurrent指向下一个节点,直到回到哨兵为止。
        pCurrent = pNext;
    }
}

Node* DoubleLinkedList::Search(int key)   //运行时间为O(n)
{
    Node* pCurrent = m_sentinel.pNext;
    while(pCurrent != &m_sentinel && pCurrent->key != key)
        pCurrent = pCurrent->pNext;
    return pCurrent == &m_sentinel ? nullptr : pCurrent;
}

void DoubleLinkedList::Insert(int key)     //运行时间为O(1)
{
    Node* pNew = new Node;                //构建新节点
    pNew->key = key;
    pNew->pPrev = &m_sentinel;             //新节点与前驱后继相互关联
    pNew->pNext = m_sentinel.pNext;
    m_sentinel.pNext->pPrev = pNew;
    m_sentinel.pNext = pNew;
}

void DoubleLinkedList::Delete(Node* p)    //运行时间为O(n)
{
    p->pPrev->pNext = p->pNext;    //将自身的前驱和后继相互关联
    p->pNext->pPrev = p->pPrev;
    delete p;                                     //删除自身
}
  • 优化方法

    Search方法的每一次循环都需要两步测试pCurrent != &m_sentinel 和 pCurrent->key != key,能不能省略前一个,只留下pCurrent->key != key?
    我们可以利用哨兵的键值,让其等于要找的key,这样一来,即使链表里没有找到key,也会在pCurrent回到哨兵的时候及时跳出循环。优化后的Search方法是这样的:

Node* DoubleLinkedList::Search(int key)
{
    Node* pCurrent = m_sentinel.pNext;
    m_sentinel.key = key;
    while(pCurrent->key != key)
        pCurrent = pCurrent->pNext;
    return pCurrent == &m_sentinel ? nullptr : pCurrent;
}
  • 带哨兵的单向循环链表
struct Node    //与双向链表不同的是,每个节点只保存一个后继节点
{
    Node* pNext;
    int key;
};

class SingleLinkedList
{
public:
    SingleLinkedList();
    ~SingleLinkedList();
    Node* Search(int key);    //Search和Insert方法的运行时间与双向链表一样,Search依然是O(n)
    void Insert(int key);        //Insert 依然是 O(1)
    void Delete(Node* p);       //但是Delete有所不同,因为删除过程需要拿到待删除元素的前驱和后继,其中前驱节点必须通过一次遍历查找得到,所以Delete的运行时间和Search一样,都是O(n)
private:
    Node m_sentinel;
};

SingleLinkedList::SingleLinkedList()
{
    m_sentinel.pNext = &m_sentinel;
}

SingleLinkedList::~SingleLinkedList()
{
    Node* pCurrent = m_sentinel.pNext;
    Node* pNext = nullptr;
    while(pCurrent != &m_sentinel)
    {
        pNext = pCurrent->pNext;
        delete pCurrent;
        pCurrent = pNext;
    }
}

Node* SingleLinkedList::Search(int key)
{
    m_sentinel.key = key;
    Node* pCurrent = m_sentinel.pNext;
    while(pCurrent->key != key)
        pCurrent = pCurrent->pNext;
    return pCurrent == &m_sentinel ? nullptr : pCurrent;
}

void SingleLinkedList::Insert(int key)
{
    Node* pNew = new Node;
    pNew->pNext = m_sentinel.pNext;
    pNew->key = key;
    m_sentinel.pNext = pNew;
}

void SingleLinkedList::Delete(Node* p)
{
    Node* pCurrent = m_sentinel.pNext;
    Node* pPrevious = &m_sentinel;
    while(pCurrent != p)
    {
        pPrevious = pCurrent;
        pCurrent = pCurrent->pNext;
    }
    pPrevious->pNext = pCurrent->pNext;
    delete pCurrent;
}
  • 将双向和单向链表加以对比,我们发现在插入和查找频繁,而删除不频繁的场景下,使用单向链表可以节省空间,而在需要频繁删除的场景下,使用双向链表,可以显著节省运行时间。

转载于:https://www.cnblogs.com/meixiaogua/p/9677957.html

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
SQLAlchemy 是一个 SQL 工具包和对象关系映射(ORM)库,用于 Python 编程语言。它提供了一个高级的 SQL 工具和对象关系映射工具,允许开发者以 Python 类和对象的形式操作数据库,而无需编写大量的 SQL 语句。SQLAlchemy 建立在 DBAPI 之上,支持多种数据库后端,如 SQLite, MySQL, PostgreSQL 等。 SQLAlchemy 的核心功能: 对象关系映射(ORM): SQLAlchemy 允许开发者使用 Python 类来表示数据库表,使用类的实例表示表中的行。 开发者可以定义类之间的关系(如一对多、多对多),SQLAlchemy 会自动处理这些关系在数据库中的映射。 通过 ORM,开发者可以像操作 Python 对象一样操作数据库,这大大简化了数据库操作的复杂性。 表达式语言: SQLAlchemy 提供了一个丰富的 SQL 表达式语言,允许开发者以 Python 表达式的方式编写复杂的 SQL 查询。 表达式语言提供了对 SQL 语句的灵活控制,同时保持了代码的可读性和可维护性。 数据库引擎和连接池: SQLAlchemy 支持多种数据库后端,并且为每种后端提供了对应的数据库引擎。 它还提供了连接池管理功能,以优化数据库连接的创建、使用和释放。 会话管理: SQLAlchemy 使用会话(Session)来管理对象的持久化状态。 会话提供了一个工作单元(unit of work)和身份映射(identity map)的概念,使得对象的状态管理和查询更加高效。 事件系统: SQLAlchemy 提供了一个事件系统,允许开发者在 ORM 的各个生命周期阶段插入自定义的钩子函数。 这使得开发者可以在对象加载、修改、删除等操作时执行额外的逻辑。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值