每日算法链表 & 例题选讲

Posted jiange_zh

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了每日算法链表 & 例题选讲相关的知识,希望对你有一定的参考价值。

单链表

链表是常用的数据结构,其优点是插入和删除元素时不需要移动,表的容量可扩充,且存储空间可以不连续。

另外,由于涉及到指针,所以很受面试官的青睐。

本文将主要介绍单链表,并简单介绍下双链表和环形链表,并通过一系列的题目来强化这方面的知识。

链表节点的结构:

template<class DataType>
struct Node
{
    DataType data;
    Node<DataType> *next;
};

data存放节点的数据,next指向下一个节点。

对于单链表,需要设置头指针,指向第一个元素所在的节点,所有的操作都是从头指针开始的。

有时候我们可以设置一个哨兵,它也是一个节点,称为头节点,该节点不存放数据,仅用于简化代码(加上头结点之后,无论链表是否为空,头指针始终指向头结点,因此空表和非空表的处理统一一点)。

下面我们将以有哨兵的链表为例来实现单链表。

template<class DataType>
class LinkList
{
    public:
        LinkList();
        LinkList(DataType a[], int n);
        ~LinkList();
        DataType get(int i); //按位查找,第i个节点
        int locate(DataType x); //按值查找,返回x的位置序号
        void insert(int i, DataType x); //在第i个位置插入x
        DataType erase(int i);
        void print();
    private:
        Node<DataType> *first; //头指针 
};

构造函数

template<class DataType>
LinkList<DataType>::LinkList()
{
    first = new Node<DataType>;
    first->next = NULL;
}

//头插法
template<class DataType>
LinkList<DataType>::LinkList(DataType a[], int n)
{
    first = new Node<DataType>;
    first->next = NULL;
    Node<DataType> *newNode;
    for (int i = 0; i < n; ++i)
    {
        newNode = new Node<DataType>;
        newNode->data = a[i];
        newNode->next = first->next;
        first->next = newNode;
    }
}

//尾插法
template<class DataType>
LinkList<DataType>::LinkList(DataType a[], int n)
{
    first = new Node<DataType>;
    Node<DataType> *rail, *newNode;
    rail = first;
    for (int i = 0; i < n; ++i)
    {
        newNode = new Node<DataType>;
        newNode->data = a[i];
        rail->next = newNode;
        rail = newNode;
    }
    rail->next = NULL;
}

析构函数

template<class DataType>
LinkList<DataType>::~LinkList()
{
    Node<DataType> *cur;
    while (first != NULL)
    {
        cur = first; //暂存释放节点
        first = first->next;
        delete cur;
    }
}

遍历操作

template<class DataType>
void LinkList<DataType>::print()
{
    Node<DataType> *cur = first->next;
    while (cur)
    {
        cout << cur->data << ‘ ‘;
        cur = cur->next;
    }
    cout << endl;
}

按位查找

template<class DataType>
DataType LinkList<DataType>::get(int i)
{
    Node<DataType> *cur = first->next;
    int pos = 1;
    while (cur && pos != i)
    {
        cur = cur->next;
        ++pos;
    }
    if (NULL == cur)
        throw "查找失败";
    else
        return cur->data;
}

按值查找

template<class DataType>
int LinkList<DataType>::locate(DataType x)
{
    Node<DataType> *cur = first->next;
    int pos = 1;
    while (cur && cur->data != x)
    {
        cur = cur->next;
        ++pos;
    }
    if (NULL == cur)
        return 0; //查找失败
    else
        return cur->data;
}

插入

template<class DataType>
void LinkList<DataType>::insert(int i, DataType x)
{
    //考虑i=1的情况,我们需要从哨兵开始
    Node<DataType> *cur = first;
    int pos = 0;
    //查找第i-1个位置
    while (cur && pos != i-1)
    {
        cur = cur->next;
        ++pos;
    }
    if (NULL == cur)
        throw "插入失败";
    else
    {
        Node<DataType> *newNode = new Node<DataType>;
        newNode->data = x;
        newNode->next = cur->next;
        cur->next = newNode;
    }
}

删除

template<class DataType>
DataType LinkList<DataType>::erase(int i)
{
    Node<DataType> *cur = first;
    int pos = 0;
    int ret;
    //查找第i-1个位置
    while (cur && pos != i-1)
    {
        cur = cur->next;
        ++pos;
    }
    if (NULL == cur || NULL == cur->next) //注意,第i-1个节点找到了,可能第i个节点不存在!
        throw "插入失败";
    else
    {
        Node<DataType> *tmpNode = cur->next; //暂存
        ret = tmpNode->data;
        cur->next = tmpNode->next; //摘链
        delete tmpNode;
        return ret;
    }
}

循环单链表

循环链表只是在单链表的基础上使其首尾相连。

对于循环链表,如果还是用first指向头指针,由于我们只有next标志,没有pre标志,所以并不能很方便地找到尾部。

因此,在循环链表中,我们常常使用尾指针rear来指示最后一个节点。如此一来,使用rear->next->next即可取得第一个节点(rear->next为哨兵),rear则取得最后一个节点,这样子对首尾的访问就便利许多。

双链表

双链表比单链表的节点多了一个prior来指向前驱节点:

template<class DataType>
struct DulNode
{
    DataType data;
    DulNode<DataType> *prior, *next;
};

双链表的大多数操作跟单链表类似,它的优点是“能进能退”,可以方便地访问前驱后继。

插入

//在p节点后插入新节点s
s->prior = p; //插入
s->next = p->next; //插入
p->next->prior = s; //换链
p->next = s; //换链

删除

//p指向待删除节点
p->prior->next = p->next;
p->next->prior = p->prior;
delete p;

静态链表

静态链表是用数组来表示链表,用数组元素的下标来模拟单链表的指针。这种表示方法比较灵活,而且速度比较快,不过空间限制比较大。

一个比较典型的例子是: 移动小球

该例可以使用两个数组left[],right[]来模拟双链表,以提高效率。

常用的静态链表存储结构:

const int MaxSize = 100;
template <class DataType>
struct SNode
{
    DataType data;
    int next
} SList[Maxsize];

静态链表需要两个指针:first为静态链表的头指针;avai是空闲链的头指针。

也就是说,我们的SList将分为两条链,一条是已使用的,一条是空闲的。

为方便运算,我们的静态链表也带上头节点。

//初始化
first = 0;
SList[first].next = -1; //已使用链只有头节点
avail = 1; //剩下的节点串成空闲链
for (int i = avail; i < MaxSize-1; ++i)
{
    SList[i].next = i+1;
}
SList[MaxSize-1].next = -1;
//在节点p后面插入新节点
if (-1 == avail)
    throw "链表已满";
int newNodeIndex = avail; //获取一个空闲的节点
avail = SList[avail].next;
SList[newNodeIndex].next = SList[p].next;
SList[p].next = newNodeIndex;
//删除节点p的后继节点
int q = SList[p].next; //暂存被删除的节点
SList[p].next = SList[q].next; //摘链
SList[q].next = avail; //删除的节点插到空闲链头部
avail = q;

**插入删除只需要修改游标,不需要移动元素。

相关题目

在O(1)时间删除链表节点

给定单向链表的头指针和一个节点指针,定义一个函数在O(1)时间删除该节点。链表节点和函数定义如下:

struct ListNode
{
    int value;
    ListNode *next;
};

void DeleteNode(ListNode** head, ListNode* p);

首先时间的限定使得我们不能从头开始遍历。

可以肯定的是,要删除节点p,我们需要让p的前驱的next指向p的后继。我们常规的想法是改变p的前驱的next,但是由于不能直接访问到,所以山不过来,我过去——将p的后继移动到p的位置上。

于是问题就很简单了:

如果p的后继存在,记为q,那么我们将q复制到p上,之后就可以对q的原位置进行解链并释放内存了,间接地删除了节点p(实际上p处的内存并没有释放,释放的是p的后继的内存)。

需要注意的特殊情况是,如果p没有后继,那么就不能用以上方法来解决了,此时仍然需要从头开始遍历。

另外一个特殊情况是,如果链表中只有一个节点,那么删除之后,需要将head置为NULL。

void DeleteNode(ListNode** pHead, ListNode* p)
{
    if (!pHead || !p)
        return;

    if (p->next) //存在后继节点
    {
        ListNode *pNext = p->next;
        p->value = pNext->value;
        p->next = pNext->next;
        delete pNext;
    }
    else if (*head == p) //只有一个节点,头节点
    {
        *head = NULL;
        delete p;
        p = NULL;
    }
    else //多个节点,删除尾节点
    {
        ListNode *pNext = *head;
        while (pNext->next != p)
            pNext = pNext->next;
        pNext = NULL;
        delete p;
        p = NULL;
    }
}

最后需要说明一点:本函数调用之前需要确保p是存在于链表中的。

倒数第k个节点

输入一个链表,输出该链表的倒数第k个节点,从1开始计数。

思路1:遍历得到链表长度n,再遍历找到第n-k+1个节点。

思路2:使用2个指针,第一个先走k-1步,之后两个指针一起走,直到第一个指针走到末尾(即其next为NULL)。

ListNode * findKthToTail(ListNode *head, unsigned int k)
{
    if (NULL == head || 0 == k) return; //注意k=0的情况
    ListNode *node1 = head, *node2 = head;
    int cnt = 0;
    while (node1 && cnt < k-1)
    {
        node1 = node1->next;
        ++cnt;
    }
    if (NULL == node1) //链表长度小于k
        return NULL;

    while (node1->next)
    {
        node1 = node1->next;
        node2 = node2->next;
    }
    return node2;
}

反转链表

假设有3个节点: pre->cur->nxt。

我们将pre->cur反转后得到 pre<-cur nxt。
中间有断开的地方,为了下次能够访问到nxt,我们必须暂存nxt,同时,为了实现反转,我们需要访问到pre,所以也需要暂存pre。于是,为实现反转,我们需要3个指针分别指向上面三者。

需要注意一些边界情况:

链表为空,链表只有1个节点。

ListNode * reverseList(ListNode *head)
{
    if (NULL == head)
        return ;
    ListNode *pre = NULL, *cur = head, *nxt = NULL;
    ListNode *reverseHead = NULL;
    while (cur)
    {
        nxt = cur->next;
        if (NULL == nxt)
            reverseHead = cur;
        cur->next = pre;
        pre = cur;
        cur = nxt;
    }
    return reverseHead;
}

关于链表的题目还有很多~这里就不一一举例了~

下一次我们将学习二叉树相关的内容!

参考资料:

《数据结构(C++版)(第2版)》 -王红梅 胡明 王涛 编著

《剑指offer》 -何海涛 著


每天进步一点点,Come on!

(●’?’●)

本人水平有限,如文章内容有错漏之处,敬请各位读者指出,谢谢!

以上是关于每日算法链表 & 例题选讲的主要内容,如果未能解决你的问题,请参考以下文章

算法_链表篇

Java算法 每日一题 编号707:设计链表

Java算法 每日一题 编号707:设计链表

Java算法 每日一题 编号707:设计链表

[每日算法220508] 单链表和双链表的反转

数据结构算法设计题