1. 程式人生 > >線性表之單鏈表的c++實現

線性表之單鏈表的c++實現

單鏈表是線性表的一種鏈式儲存結構,它同順序表(由於順序表實現比較簡單,本文不做講述了)不同,是用一組任意地址的儲存單元,存放線性表中的元素。為了表示結點之間的關係,每個結點不僅要儲存它表示的元素,還要儲存它下一個結點的資訊。

下面我們用C++的模版來實現以下單鏈表:

結點定義

template <class T>
struct Node
{
	Node* pNextNode;
	T m_value;
};
單鏈表定義:
template <class T>
class CLinkList
{
private:
	Node<T>* m_pHead;
	int m_nLength; // 單鏈表長度
// 一元多項式求和
	friend void Add(CLinkList<elem> & A, CLinkList<elem>& B);</span>
public:
	friend void Circle(int n, int m);
// 約瑟夫環
	CLinkList();</span>
	// 初始化含有n個元素,值為value的單鏈表n>=1
	CLinkList(int n, T value);
	~CLinkList();
	// 求單鏈表長度
	int GetLength();
	// 刪除單鏈表的第i個結點,返回刪除結點的值
	T Delete(int i);
	// 在單鏈表中第i個位置插入值為x的元素
	void Insert(int i, T x);
	// 返回值為value的序號
	int GetValue(T value);
	// 返回第i個元素的值
	T GetLocate(int i);
	// 單鏈表遍歷
	void PrintList();
	// 單鏈表逆序
	void Reverse();
將當連結串列轉換成環,此方法使用之後,只能使用Delete(i)方法;此方法是為了解決約瑟夫環問題
	void Ring();
};
template <class T>
CLinkList<T>::CLinkList()
{
m_pHead = NULL;
}

1)關於單鏈表的建立
單鏈表的建立有兩種方法:頭插法:新插入的元素始終為插入的第一個元素建立過程:初始化頭指標為NULL,每建立一個元素,使其指向頭指標,將頭指標指向這個新建立的元素.
尾插法:先建立單鏈表頭指標元素,建立一個尾指標,指向當前元素.每建立一個元素,將尾指標元素的下一個結點指向建立元素,尾指標指向當前元素.
template <class T>
CLinkList<T>::CLinkList(int n, T value)
{
	if (n < 1)
	{
		m_pHead = NULL;
		return;
	}
	// 頭插法建立單鏈表
	/*for (int i = 0; i<n; i++)
	{
		Node* pNode = new Node<T>;
		pNode->m_value = value;
		pNode->pNextNode = pHead;
		pHead = pNode;
	}*/
	// 尾插法建立單鏈表
	m_pHead = new Node<T>;
	m_pHead->m_value = value++;
	m_pHead->pNextNode = NULL;
	Node<T>* pTail = m_pHead;
	for (int i = 1; i<n; i++)
	{
		Node<T>* pWork = new Node<T>;
		pWork->m_value = value++;
		pWork->pNextNode = NULL;
		pTail->pNextNode = pWork;
		pTail = pWork;
	}
}


2)求單鏈表長度
從頭結點遍歷到尾結點,獲取長度.
<pre name="code" class="cpp">template <class T>
int CLinkList<T>::GetLength()
{
	if (m_pHead == NULL)
	{
		return 0;
	}
	int i = 0;
	Node<T>* r = m_pHead;
	while (r != NULL)
	{
		i++;
		r = r->pNextNode;
	}
	return i;
}

3)刪除指定位置的連結串列結點,返回刪除結點的值
要特別注意刪除位置是頭結點的情況,需要將頭指標重新指向
另外為了刪除結點,還要儲存當前刪除結點的前驅結點資訊.
<pre name="code" class="cpp">template <class T>
T CLinkList<T>::Delete(int i)
{
	if (i <= 0 || m_pHead == NULL)
	{
		CError error("刪除位置異常");
		throw error;
	}
	// 頭指標刪除
	if (i == 1)
	{
		Node<T>* r = m_pHead;
		m_pHead = m_pHead->pNextNode;
		int nValue = r->m_value;
		delete r;
		return nValue;
	}
	Node<T>* r = m_pHead;
	Node<T>* prev = m_pHead;
	while (--i && r != NULL)
	{
		prev = r;
		r = r->pNextNode;
	}
	if (r == NULL)
	{
		CError error("連結串列未初始化");
		throw error;
	}
	// r是要刪除的結點,prev是前置結點
	int value = r->m_value;
	prev->pNextNode = r->pNextNode;
	delete r;
	return value;
}

4)在位置i插入值為x的結點
<pre name="code" class="cpp">template <class T>
void CLinkList<T>::Insert(int i, T x)
{
	if (i <= 0)
	{
		CError error("插入位置異常!");
		throw  error;
	}
	if (m_pHead == NULL)
	{
		m_pHead = new Node<T>;
		m_pHead->m_value = x;
		m_pHead->pNextNode = NULL;
		return;
	}
	// 尋找插入結點
	Node<T>* r = m_pHead;
	Node<T>* prev = m_pHead;
	while (--i && r!=NULL)
	{
		prev = r;
		r=r->pNextNode;
	}
	if (r == NULL) // 此種情況認為插在隊尾
	{
		r = new Node<T>;
		r->m_value = x;
		r->pNextNode = NULL;
		prev->pNextNode = r;
		return;
	}
	// 如果是在頭結點插入,將頭指標指向這個結點,這個結點下一個為原來的頭指標
	if (r == m_pHead)
	{
		Node<T>* pNode = new Node<T>;
		pNode->m_value = x;
		pNode->pNextNode = r;
		m_pHead = pNode;
		return;
	}

	Node<T>* pNode = new Node<T>;
	pNode->m_value = x;
	pNode->pNextNode = r;
	prev->pNextNode = pNode;
	return;
}

5)獲取指定位置的元素值
template <class T>
T CLinkList<T>::GetLocate(int i)
{
	Node<T>* pNode = m_pHead;
	while (--i && pNode != NULL)
	{
		pNode = pNode->pNextNode;
	}
	if (pNode == NULL)
	{
		CError error("查詢位置非法");
		throw error;
	}
	int nValue = pNode->m_value;
	return nValue;
}

6)獲取值為value元素的位置
template <class T>
int CLinkList<T>::GetValue(T value)
{
	int j = 1;
	Node<T>* pNode = m_pHead;
	while ( pNode != NULL )
	{
		if (pNode->m_value == value)
		{
			return j;
		}
		j++;
		pNode = pNode->pNextNode;
	}
	// 沒有找到
	return -1;
}

7)單鏈表遍歷輸出
template <class T>
void CLinkList<T>::PrintList()
{
	Node<T>* pNode = m_pHead;
	while (pNode != NULL)
	{
		printf("%d\r\n", pNode->m_value);
		pNode = pNode->pNextNode;
	}
}

8)單鏈表析構釋放記憶體template <class T>
CLinkList<T>::~CLinkList()
{
Node<T>* pNode = m_pHead;
while (pNode != NULL)
{
Node<T>* r = pNode;
pNode = pNode->pNextNode;
delete r;
}
m_pHead = NULL;
}9)單鏈表逆序
template <class T>
void CLinkList<T>::Reverse()
{
	Node<T>* pPrev = NULL;
	Node<T>* pCur = m_pHead;
	while(pCur!=NULL && pCur->pNextNode != NULL)
	{
		Node<T>* next = pCur->pNextNode;
		pCur->pNextNode = pPrev;
		pPrev = pCur;
		pCur = next;
	}
	pCur->pNextNode = pPrev;
	m_pHead = pCur;
}

10)使單鏈表形成環,為了解決一些環形連結串列的問題。但是將連結串列轉換為環後,關於單鏈表尾指標後繼結點為NULL的判斷將不成立,
所以一些插入,刪除等方法將不能是用,必須注意!!!
<pre name="code" class="cpp">template <class T>
void CLinkList<T>::Ring()
{
	if (m_pHead == NULL)
	{
		return;
	}
	// 找到尾結點
	Node<T>* r = m_pHead;
	while (r->pNextNode != NULL)
	{
		r = r->pNextNode;
	}
	// 找到了尾結點,使其指向頭結點
	r->pNextNode = m_pHead;
}

11)友元函式實現一元多項式求和
一個單鏈表的實際應用,一元多項式求和.(這個有點問題,以後有機會再修改,大致思路是這樣的)
<pre name="code" class="cpp">void Add(CLinkList<elem> & A, CLinkList<elem>& B)
{
	Node<elem>* pa = A.m_pHead;
	Node<elem>* pb = B.m_pHead;
	Node<elem>* pPrevA = NULL;
	Node<elem>* pPrevB = NULL;
	while (pa != NULL && pb != NULL)
	{
		// 如果A係數大於B, 將B元素插入到A當前元素的前邊
		if (pa->m_value.exp > pb->m_value.exp)
		{
			Node<elem>* r = new Node<elem>;
			r->m_value.coef = pb->m_value.coef;
			r->m_value.exp = pb->m_value.exp;
			if (pa == A.m_pHead) // 頭結點之前插入,要改變頭指標
			{
				r->pNextNode = pa;
				A.m_pHead = r;
				pPrevA = r;
				pPrevB = pb;
				pb = pb->pNextNode;
			}else // 如果不是頭指標
			{
				r->pNextNode = pa;
				pPrevA->pNextNode = r;
				pPrevA = r;
				pPrevB = pb;
				pb = pb->pNextNode;
			}
			// 將b元素刪除
			if (pb == B.m_pHead )
			{
				Node<elem>* s = pb;
				pb = pb->pNextNode;
				B.m_pHead = pb;
				pPrevB = NULL;
				delete s;
			}else
			{
				Node<elem>* s = pb;
				pb = pb->pNextNode;
				pPrevB->pNextNode = pb;
				delete s;
			}

		}else if (pa->m_value.exp < pb->m_value.exp) // 如果A係數小於B,比較A下一個元素
		{
			pPrevA = pa;
			pa = pa->pNextNode;
		}else // 如果相等,求和
		{
			pa->m_value.coef += pb->m_value.coef;
			if (pa->m_value.coef == 0) // 如果係數為0,刪除pa當前元素
			{
				if (pa == A.m_pHead)  
				{
					A.m_pHead = pa->pNextNode;
					Node<elem>* s = pa;
					pa = pa->pNextNode;
					pPrevB = pb;
					pb = pb->pNextNode;
					delete s;
				}else
				{
					pPrevA->pNextNode = pa->pNextNode;
					pPrevA = pa;
					Node<elem>* s = pa;
					pa = pa->pNextNode;
					pPrevB = pb;
					pb = pb->pNextNode;
					delete s;
				}
			}else 
			{
				pPrevA = pa;
				pPrevB = pb;
				pa = pa->pNextNode;
				pb = pb->pNextNode;
			}
		}
	}
	if (pb != NULL)
	{
		pPrevA->pNextNode = pb;
	}
}

12)約瑟夫環問題
void Circle(int n, int m)
{
	CLinkList<int> MyLinkList;
	for (int i = 1; i<=n; i++)
	{
		MyLinkList.Insert(i, i);
	}
	printf("報數:");
	MyLinkList.PrintList();
	// 環
	MyLinkList.Ring();
	printf("報%d出圈,出圈次序:", m);
	int k = m;
	Node<int>* pWork = MyLinkList.m_pHead;
	Node<int>* pPrev = pWork;
	for (int i = 0; i<n; i++)
	{
		while (--k)
		{
			pPrev = pWork;
			pWork = pWork->pNextNode;
		}
		k = m;
		printf("出圈%d, ", pWork->m_value);
		Node<int>* s = pWork;
		pPrev->pNextNode = pWork->pNextNode;
		pWork = pWork->pNextNode;
		delete s;
	}
	MyLinkList.m_pHead = NULL;
}

注:紅色功能是複雜的功能,不是單鏈表的必有實現.

單鏈表是鏈式儲存結構,為了查詢第i個元素,必須先找到i-1個元素.不同於順序表的隨機存取結構.

下面是百度轉載的一份關於順序表和單鏈表的比較:

在C++ STL中vector和list的實現,類似於順序表和單鏈表,兩種結構什麼情況使用也可以參照下面的比較.

順序表與連結串列的比較

一、順序表的特點是邏輯上相鄰的資料元素,物理儲存位置也相鄰,並且,順序表的儲存空間需要預先分配。

它的優點是:

  (1)方法簡單,各種高階語言中都有陣列,容易實現。

  (2)不用為表示節點間的邏輯關係而增加額外的儲存開銷。

  (3)順序表具有按元素序號隨機訪問的特點。

缺點:

  (1)在順序表中做插入、刪除操作時,平均移動表中的一半元素,因此對n較大的順序表效率低。

  (2)需要預先分配足夠大的儲存空間,估計過大,可能會導致順序表後部大量閒置;預先分配過小,又會造成溢位。

二、在連結串列中邏輯上相鄰的資料元素,物理儲存位置不一定相鄰,它使用指標實現元素之間的邏輯關係。並且,連結串列的儲存空間是動態分配的。

連結串列的最大特點是:

  插入、刪除運算方便。

缺點:

  (1)要佔用額外的儲存空間儲存元素之間的關係,儲存密度降低。儲存密度是指一個節點中資料元素所佔的儲存單元和整個節點所佔的儲存單元之比。

  (2)連結串列不是一種隨機儲存結構,不能隨機存取元素。

三、順序表與連結串列的優缺點切好相反,那麼在實踐應用中怎樣選取儲存結構呢?通常有以下幾點考慮:

  (1)順序表的儲存空間是靜態分配的,在程式執行之前必須明確規定它的儲存規模,也就是說事先對“MaxSize”要有合適的設定,設定過大會造成儲存空間的浪費,過小造成溢位。因此,當對線性表的長度或儲存規模難以估計時,不宜採用順序表。然而,連結串列的動態分配則可以克服這個缺點。連結串列不需要預留儲存空間,也不需要知道表長如何變化,只要記憶體空間尚有空閒,就可以再程式執行時隨時地動態分配空間,不需要時還可以動態回收。因此,當線性表的長度變化較大或者難以估計其儲存規模時,宜採用動態連結串列作為儲存結構。

  但在連結串列中,除資料域外海需要在每個節點上附加指標。如果節點的資料佔據的空間小,則連結串列的結構性開銷就佔去了整個儲存空間的大部分。當順序表被填滿時,則沒有結構開銷。在這種情況下,順序表的空間效率更高。由於設定指標域額外地開銷了一定的儲存空間,從儲存密度的角度來講,連結串列的儲存密度小於1.因此,當線性表的長度變化不大而且事先容易確定其大小時,為節省儲存空間,則採用順序表作為儲存結構比較適宜。

  (2)基於運算的考慮(時間)

  順序儲存是一種隨機存取的結構,而連結串列則是一種順序存取結構,因此它們對各種操作有完全不同的演算法和時間複雜度。例如,要查詢線性表中的第i個元素,對於順序表可以直接計算出a(i)的的地址,不用去查詢,其時間複雜度為0(1).而連結串列必須從連結串列頭開始,依次向後查詢,平均需要0(n)的時間。所以,如果經常做的運算是按序號訪問資料元素,顯然順表優於連結串列。

  反之,在順序表中做插入,刪除時平均移動表中一半的元素,當資料元素的資訊量較大而且表比較長時,這一點是不應忽視的;在連結串列中作插入、刪除,雖然要找插入位置,但操作是比較操作,從這個角度考慮顯然後者優於前者。

  (3)基於環境的考慮(語言)

  順序表容易實現,任何高階語言中都有陣列型別;連結串列的操作是基於指標的。相對來講前者簡單些,也使用者考慮的一個因素。

  總之,兩種儲存結構各有長短,選擇哪一種由實際問題中的主要因素決定。通常“較穩定”的線性表,即主要操作是查詢操作的線性表,適於選擇順序儲存;而頻繁做插入刪除運算的(即動態性比較強)的線性表適宜選擇鏈式儲存。