【每日演算法】連結串列 & 例題選講
單鏈表
連結串列是常用的資料結構,其優點是插入和刪除元素時不需要移動,表的容量可擴充,且儲存空間可以不連續。
另外,由於涉及到指標,所以很受面試官的青睞。
本文將主要介紹單鏈表,並簡單介紹下雙鏈表和環形連結串列,並通過一系列的題目來強化這方面的知識。
連結串列節點的結構:
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!
(●’◡’●)
本人水平有限,如文章內容有錯漏之處,敬請各位讀者指出,謝謝!