1. 程式人生 > >【每日演算法】連結串列 & 例題選講

【每日演算法】連結串列 & 例題選講

單鏈表

連結串列是常用的資料結構,其優點是插入和刪除元素時不需要移動,表的容量可擴充,且儲存空間可以不連續。

另外,由於涉及到指標,所以很受面試官的青睞。

本文將主要介紹單鏈表,並簡單介紹下雙鏈表和環形連結串列,並通過一系列的題目來強化這方面的知識。

連結串列節點的結構:

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!

(●’◡’●)

本人水平有限,如文章內容有錯漏之處,敬請各位讀者指出,謝謝!