數據結構淺淺析之(一)——線性表(List 附C++代碼)

一.寫在前面

“生活不止眼前的苟且,還有詩和遠方的田野,你赤手空拳的來到人世間,爲找到那篇海不顧一切”,高曉松說。我們學習開發這麼多年,也明白“開發不止當下的bug,還有將來和未發現的bug,我們在開發的路上不斷探索,只爲找尋那優質的產品”。開發猶練功,可分爲外功招式和內功心法,自計算機問世以來,曾經出現了好多外功招式(編程語言)以及內功心法(數據結構)。各大外功招式都曾名噪一時,有被時間遺忘的,有曾曇花一現的,也有經久不衰的。語言總體分爲彙編語言,機器語言,腳本語言和高級語言。而我們所常見的語言有C、C++、C#、Java、python、php、易語言等等,不管是像C和Java這類編譯型語言還是像python這類解釋型語言,他們都有一個相通的東西,那就是編程思想。語言只是工具,思想纔是精髓。而在編程興起的長河中,有一套基本適用於大部分語言的內功心法——數據結構。本篇文章以線性表(List)開始說起

二.基本概念

  • 基本定義

      線性表(List):零個或多個數據元素的有限序列。

  •  關鍵點

1.首先線性表是一個序列:元素之間有確定的順序;

2.線性表是有限的:元素的個數n爲線性表的長度,當n爲0時,記爲空表。

三. 線性表的存儲結構

  順序存儲結構

       1. 基本知識   

  1. 用一段地址連續的存儲單元依次存儲線性表的數據元素

    線性表(a1,a2,……an)的順序存儲示意圖如下:

        順序存儲結構簡單說就是在內存中開闢了一塊內存,按照依次佔位的方式存入元素。

      2.插入與刪除    

          獲取操作:線性存儲結構獲取元素非常簡單,我們只需要傳入我們想獲取的第幾個元素,線性表就會根據下標找到這個元 素。

          插入操作 :     

插入操作的思路如下:

  •  如果插入位置不合理,則拋出異常;
  •  如果線性表長度大於等於數組長度,則拋出異常或者動態增加容量;
  • 從最後一個元素開始向遍歷到第i個位置,分別對它們都向後移動一個位置;
  • 將要插入的元素填入到第i個位置;
  • 表長加1。

        刪除操作:

刪除操作的思路如下:

  •  如果刪除位置不合理,則拋出異常;
  •  取出刪除元素;
  • 從刪除元素位置開始遍歷到最後一個元素位置,分別將它們向前移動一個位置;
  • 表長減1。

 線性表的順序存儲結構,在存、讀數據元素時,它的時間複雜度爲O(1),插入和山刪除時,時間複雜讀度爲O(n)。因此這種存儲方式適合元素個數不太變換,更多的操作是存讀數據的應用。

    3.優缺點

                    優點                 缺點
  • 可以快速的存取表中的任意元素
  • 無需爲表中邏輯關係而增加額外存儲空間
  • 插入和刪除操作需要移動大量的元素
  • 容易造成存儲空間碎片
  •  線性表的鏈式存儲結構

        前面已經提到,在線性表的順序存儲結構中,當對錶中元素進行插入與刪除操作時需要改動大量的

 元素來完成操作,因而容易耗費大量的時間。因此我們追尋一種能提升工作效率的方法。

      1.基本知識

       在線性表的鏈式存儲結構中,每個數據元素ai與其直接後繼數據元素ai+1之間的邏輯關係除了存儲其本身的信息之外,還需要存存儲一個指示其直接後繼的信息(即直接後繼的存儲位置)。我們把存儲數據元素信息的域稱爲數據域,把存儲直接後繼位置的域稱爲指針域。指針域中存儲的信息稱爲指針或鏈。這兩部分信息組成數據元素ai的存映射,稱爲結點(Node)

     n個結點鏈成一個鏈表,即爲線性表的鏈式存儲結構,因爲此鏈表的每個結點只包含一個指針域,所以叫單鏈表。如下圖所示:

我們把鏈表中第一個結點的存儲位置叫做頭指針

頭指針與頭結點的區別:

                                  頭指針                                 頭結點
  • 頭指針是指鏈表指向的第一個結點的指針,若鏈表有頭結點,則是指向頭結點的指針
  • 頭指針具有標識作用
  • 無論鏈表是否爲空,頭指針不爲空,頭指針是鏈表的必要元素
  • 頭結點是爲了操作統一而設立的,放在第一元素的結點之前,其數據域一般無意義
  • 有了頭結點,在第一結點前插入和刪除第一節點就和其他結點相統一了
  • 頭結點不一定是鏈表的必要元素

  2.獲取、插入與刪除   

    在線性鏈表的存儲結構中,我們要計算任何一個元素是很容易的,但在鏈式存儲方式中,無法知道第i個元素在哪裏,必須從頭開始查找。

獲取第i個元素的思路:

  • 聲明一個節點p指向鏈表的第一個節點,初始化j從開始;
  • 當j<i時,就遍歷鏈表,讓p的指針向後移動,不斷指向下一節點,j累加1;
  • 若到鏈表末尾p爲空,則說明第i個元素不存在;
  • 否則查找成功,返回節點p的數據。

在進行單鏈表的插入時,假設存儲元素e的結點爲s,要實現結點p、p->next和s之間邏輯關係的變化,只需要

將結點s插入到結點p和p->next之間即可。其他結點無需更改。

單鏈表第i個數據插入結點的算法思路:

  • 聲明一結點p指向鏈表的第一個結點,初始化j從1開始
  • 當j<i時,就遍歷鏈表,讓p的指針向後移動,不斷指向下一結點,j累加1
  • 若到鏈表末尾p爲空,則說明第i個元素不存在
  • 否則查找成功,在系統中生成一個空結點s
  • 將數據元素e賦值給s->data
  • 單鏈表的插入標準語句爲s->next=p->next;p->next = s
  • 返回成功

在進行單鏈表刪除時,設存儲元素ai的結點爲q,要實現將結點q刪除操作,其實就是將它的前繼結點的指針繞過,指向它的後繼結點即可。如下圖:

 

單鏈表第i個數據刪除思路:

  • 聲明結點p指向鏈表第一個結點,初始化j從1開始
  • 當j<i時,就遍歷鏈表,讓p的指針向後移動,不斷指向下一節點,j累加1
  • 若到鏈表末尾p爲空,則說明第i個元素不存在
  • 否則查找成功,將欲刪除的結點p->next賦值給q
  • 單鏈表的刪除標準語句p->next=q->next
  • 將q結點中的數據賦值給e,作爲返回
  • 釋放q結點
  • 返回成功

我們發現單鏈表的插入和刪除操作,它們都有兩部分組成:第一部分是遍歷查找第i個元素;第二部分是插入和刪除元素。它們的時間複雜度都是O(n)。很顯然對於插入或者刪除數據比較頻繁的操作時,單鏈表的效率明顯高於順序方式存儲。

  • 單鏈表結構與順序存儲結構比較

單鏈表結構與順序存儲結構比較

  存儲分配方式              時間性能 空間性能    查找     插入和刪除 順序存儲結構 連續的一段存儲單元依次存儲    O(1) 需要平均移動表長一半的元素,時間爲O(n) 需要預分配存儲空間,分大了,浪費,分小了,溢出 鏈式存儲結構 一組任意的存儲單元存放    O(n) 在找出某位置指針後,時間爲O(1) 不需要預分配,只要有就分配,元素個數不受限

在C/C++語言陣營裏因爲具有指針的能力,可以很方便的進行鏈表的操作,而在java,C#陣營裏,雖然不使用指針,但因爲啓用了引用機制,從某種角度也間接實現了指針的一些作用,而像早起的Basic,Fortran由於沒有指針,它們用數組來代替,通常我們把這種用數組描述的鏈表叫做靜態鏈表

  • 循環鏈表

對於單鏈表而言,由於每個結點只存儲了向後的指針,到了尾標誌就停止了。如果我們將單鏈表的末尾結點的指針端由空指針改爲指向頭結點,那麼就使整個單鏈表形成一個環,生生不息,無盡無窮,這種頭尾相連的單鏈表稱爲單循環鏈表,簡稱循環鏈表(circular linked list)。如圖:

  • 雙向鏈表

鏈表中的結點只包含一個指向後繼的指針我們稱爲單鏈表,當我們在設置一個指向前驅結點的指針域,它就成了雙向鏈表。所以在雙向鏈表中的結點都有兩個指針域,一個指向直接後繼,另一個指向直接前驅。如圖:

三.實例代碼

話不多說直接上代碼:

#include<stdlib.h>
#include <iostream>

using namespace std;

//首先定義一個結點
struct Node
{
	int data;
	Node* next;
};

//定義一個鏈表類
class ListLink
{
public:
	ListLink();
	~ListLink();

public:
	bool clearList();
	inline bool isEmpty() { return m_head == NULL; }
	int Length();
	bool GetElem(const int index, int *data);     //獲取元素
	bool Insert(const int index, int data);       //插入元素
	bool Delete(const int index, int *data);      //刪除元素
private:
	Node* m_head;
};

ListLink::ListLink()
	:m_head(NULL)
{

}

ListLink::~ListLink()
{
	Node *p = m_head;
	while (m_head)
	{
		p = m_head;
		m_head = m_head->next;
		delete(p);
	}
}

bool ListLink::clearList()
{
	Node *p = m_head;
	while (m_head)
	{
		p = m_head;
		m_head = m_head->next;
		delete(p);
	}
	return true;
}

int ListLink::Length()
{
	Node *p = m_head;
	int len = 0;
	while (p != NULL)
	{
		len++;
		p = p->next;
	}
	return len;
}

bool ListLink::GetElem(const int index, int *data)
{
	Node *p = m_head;
	int j = 0;
	while (p&&j < index)
	{
		p = p->next;
		j++;
	}
	if (p == nullptr) 
	{
		return false;
	}
	*data = p->data;
	return true;
}

bool ListLink::Insert(const int index, int data)
{
	Node *p = m_head;
	Node *s;
	int j = 0;
	if (index == 0)
	{
		s = (Node *)new Node[1];
		s->data = data;
		s->next = p;
		m_head = s;
		return true;
	}
	while (p&&j < index - 1)
	{
		p = p->next;
		j++;
	}
	if (p == NULL)
	{
		return false;//到隊尾了
	}	
	s= (Node *)new Node[1];
	s->data = data;
	s->next = p->next;
	p->next = s;
	return true;
}

bool ListLink::Delete(const int index, int *data)
{
	Node *p = m_head; 
	Node *s;
	if (p == NULL)
	{
		return false;
	}
	int j = 0;
	if (index == 0)
	{
		m_head = m_head->next;
		*data = p->data;
		delete p;
		p = NULL;
		return true;
	}
	while (p&&j < index - 1)
	{
		j++;
		p = p->next;
	}
	if (p == NULL)
		return false;
	s = p->next;
	p->next = p->next->next;
	*data = s->data;
	delete s;
	s = NULL;
	return true;
}

//主函數測試
int _tmain(int argc, _TCHAR* argv[])
{
	int a = 0;
	int *p = &a;
	ListLink list;

	//插入測試
	list.Insert(0, 1);
	list.Insert(1, 2);
	list.Insert(2, 3);
	list.Insert(3, 4);
	list.Insert(3, 5);
	list.Insert(1, 6);

	//鏈表長度
	cout <<"鏈表長度:="<< list.Length()<<endl;

	//插入的元素值
	cout << "各個元素的值依次是:"<< endl;
	for (int i = 0;i < list.Length();i++)//遍歷該鏈表
	{
		if (list.GetElem(i, p))
		{
			cout << *p<<endl;
		}	
	}
	cout << endl;

	//刪除元素
	int e = 3;
	list.Delete(2,&e);

	//鏈表長度
	cout <<"刪除元素後鏈表長度:="<< list.Length()<<endl;

	//刪除元素之後剩下的元素
	cout << "刪除元素之後剩下各個元素的值依次是:"<< endl;
	for (int i = 0;i < list.Length();i++)//遍歷該鏈表
	{
		if (list.GetElem(i, p))
		{
			cout << *p<<endl;
		}	
	}
	cout << endl;

	//清空鏈表
	list.clearList();
	//鏈表長度
	cout <<"清空後鏈表長度:="<< list.Length()<<endl;

	system("pause");
}

程序輸出測試結果如下:

 

}

【下一篇:】數據結構淺淺析之(二)——棧和隊列(Stack && Queue):https://blog.csdn.net/weixin_39951988/article/details/86518425

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章