一、概述
定義
數據結構研究的是:我們如何把現實中大量而複雜的問題以特定的數據類型和特定的存儲結構保存到主存儲器(內存)中,以及在此基礎上爲實現某個功能(比如查找、刪除某個元素)而執行的相應操作(這個操作也叫做算法)
總得來說,數據結構研究的是數據的存儲和對數據的操作。
其中,數據的存儲包括數據個體的存儲、個體間關係的存儲,我們認爲個體關係的存儲更爲重要,系數據結構所要研究的一個重點。
整體框架
既然要研究數據的存儲,那麼我們就需要創造一些存儲結構,這些存儲結構既要能夠滿足存儲需求,又要方便存儲數據之間的關係。
我們將存儲結構分爲線性結構和非線性結構,劃分的依據爲結構是否爲一維的。其中,線性結構包括數組、鏈表,非線性結構包括樹和圖。對於線性結構,其常見應用爲棧和隊列。這裏我主要詳細學習了鏈表、棧、隊列,粗略學習了樹。
在正式學習數據結構之前,需要預備和鞏固一些C語言的知識,如指針、結構體、動態內存的分配:
指針是C語言的靈魂,有強大的用處,且所有指針變量只佔4個字節。指針變量是存放內存單元地址的變量,或者說,指針就是地址,地址就是指針。我個人覺得對於指針,心中一定要有這樣的概念:
指針可以用於傳入函數並取出函數中的某個數值、
傳指針比傳入整個結構更方便、
通過對指針的操作能夠間接對該指針指向的某個變量進行操作
而結構體則是爲了表示一些複雜的數據(普通的基本變量類型無法滿足需求)。能熟悉結構體的基本操作就行了,諸如訪問其成員變量之類的。
研究動態內存分配有以下幾個原因
可以通過malloc得到一塊內存空間,並以此方法得到大量(許多塊)離散的空間。那麼我們就可以通過malloc來模擬一些離散結構或者連續結構。
函數調用中,子函數生成的變量存在作用域,所生成的變量是靜態的,有生存期,而malloc生成的內存空間只要沒有free掉,就是一直存在的,動態的。那麼我們就可以通過malloc生成一些不被自動清除的存儲結構。
有了這些簡單的理解就可以開始學習數據結構了。
二、線性結構
連續存儲:數組
這個沒啥好說的,都已經會了。
離散存儲:鏈表
由於有時候我們無法得到一塊較大且連續的內存空間,但是我們可以得到同樣內存大小只不過是離散的空間。於是有了鏈表。
鏈表簡單來說就是先定義一個個"節點"的存儲結構,然後用指針把這些結構依次聯繫起來,得到一條線性的鏈,即爲鏈表。具體的定義爲:
n個節點離散分配,彼此通過指針相連,每個節點只有一個前驅節點和一個後續節點,首節點沒有前驅節點,尾結點沒有後續節點
於是我們就要研究如何去構建一個"節點"的存儲結構:定義一個結構體,其中包含一個數據域和一個指針域,數據域可以是簡單的一個int型的數據,也可以是複雜的。指針域可以是兩個指針,用於指向該節點的前後兩個節點,也可以只用一個指針,指向該節點的後續節點。(兩個指針的那種稱爲雙向鏈表,一個指針的爲單向鏈表,這裏我們只以單向鏈表爲例)
//創建一個結構體來表示單鏈表的節點
typedef struct Node
{
int data; //數據域
struct Node * pNext; //指針域
}NODE,*pNODE; //NODE等價於 struct Node,pNODE等價於struct Node *
注意這裏指針的類型也是一個節點Node類型,因爲該指針待指向的就是Node類型的節點,所以存儲的也是Node類型的地址,自然用的是Node *。
有了單個"節點"結構,怎麼把整個鏈表鏈接起來呢?就用指針,每個節點的指針指向下一個節點,以此類推,就實現了鏈表的鏈接。這裏不展開,詳細代碼見附帶文件。
這裏要提到一點,在首節點前,還有一個頭節點,不存放有效數據,只是爲了方便鏈表的一些操作。
於是通過指向鏈表頭節點的指針,我們就可以獲取到整個鏈表。但是,由於是單鏈表,我們要對鏈表進行某些操作(插入節點、刪除節點)時,必須從頭開始往後遍歷。
具體代碼見附帶文件。
鏈表應用之棧
棧是在鏈表的基礎上形成的一種存儲結構,可以理解爲一種容器,一種可以實現“先進後出”的存儲結構,類似於子彈彈夾。
棧這種存儲類型簡單來說就是定義兩個指針,用來表示棧頂和棧底,然後用節點去填充這個棧,每填充一個節點,該節點的指針指向先前的棧頂的節點,同時,指向棧頂的指針跟着移位指向新的棧頂。
具體代碼定義如下:
typedef struct Stack
{
pNODE pTop; //棧的頂部
pNODE pBottom; //棧的底部
}STACK,*pSTACK; //STACK等價於 struct Stack,pSTACK等價於struct Stack *
這裏可以這樣理解,其實棧只是提供了兩塊鐵片,實際存儲單元還是節點,我們只不過用這兩塊鐵片分別放在這些節點的頂部和底部,"方便"我們操作而已。所以我們可以把棧看做是操作受限的鏈表(只能從棧頂對其進行操作),諸如壓棧、出棧其實就是鏈表的增加一個節點和刪除一個節點的操作,只不過多了兩個棧指針,所以稍微複雜了一些。
棧遵循"先進後出",或者說"後進先出",具體應用有諸如函數調用、中斷、內存分配等。
具體代碼見附帶文件。
鏈表應用之隊列
隊列是一種可以實現“先進先出”的存儲結構。
這裏以靜態隊列爲例,用數組實現。(鏈式隊列——用鏈表實現,這裏不談,但是隊列是鏈表的常用應用)
靜態隊列爲了避免內存空間的浪費,通常都採用循環隊列。於是我們可以把隊列想象成一個輪盤、一個左輪手槍的子彈槽。與棧類似,我們有兩個指針,一個用於指向新增添的元素的下一位,一個用於指向新刪去的元素的下一位。
一開始,兩個指針同時指向起始位置(數組下標設爲零)。每增加一個元素(即增加節點),存放在名爲rear的指針所指向的位置,且rear指向下一位。每刪除一個元素(即刪除節點),刪除名爲front的指針所指向位置的節點,且front指向下一位。則front和rear就在元素增減的過程中追趕。由於是循環隊列,所以整個"轉盤"一直轉圈,內存空間恆定,不會存在內存泄露或者浪費的問題。
同時,我們也能發現,我們只能從rear所在位置增加元素,只能從front所在位置刪除元素,先進入的節點最接近front最先被刪除,是一種"先進先出"的結構。而且它其實也是一種操作受限的鏈表。
由於是先進先出,所以所有和時間有關的操作都有隊列的影子,根據時間順序先幹嘛後幹嘛…
專題:遞歸
其定義是一個函數直接或間接調用自己。
遞歸的思想個人總結爲:任務複雜,能力不足,任務下推
即,儘管任務很龐大,我只管處理我所能夠處理的最小一部分,其餘部分交給"另一個未來的我"去處理。
或者說,一個任務實現的前提,是完成一個難度稍微降低的同一任務,且存在一個最底層的任務能夠輕易完成。
遞歸的應用:樹和森林就是以遞歸的方式定義的,樹和圖的很多算法都是以遞歸來實現的
三、非線性結構
非線性結構包括樹和圖,只學了樹,這裏只討論二叉樹。
其實和前面的鏈表差不多的是,基礎結構還是節點,只不過這個節點的指針域需要有兩個指針,用來指向左孩子和右孩子節點。然後類似鏈表,通過指針鏈接起來就行了。
這裏,我們回顧對比一下之前的鏈表、棧、隊列。
我們之前提到,數據存儲中,我們認爲個體關係的存儲更爲重要,系數據結構所要研究的一個重點。
這裏樹是非線性結構,各個節點之間的關係不是一維的,不是單純的線性。這樣的話,樹的存儲會有什麼不同嗎?或者說,我們又如何處理各個節點的關係呢?
其實這個問題我們在生成樹的基本單元——節點——的時候,就已經解決了。每個節點都存放着兩個指針,指向該節點的孩子節點。只要指針指好了,個體關係就確定好了。那麼雖然說各個節點是離散的,只管存放就是了,節點的關係已經保存在每個節點中了。
也就是說,儘管我們每個節點可能是按照隨意順序存放的,但是隻要拿出來,還是一整棵完整的樹,關係通過存放在節點中的指針保存。
接下來又有一個問題,個體間關係是保存起來了,我們怎麼表現出來呢?如果我們羅列出樹的所有元素,我們怎麼把關係也同時展示出來呢?
我們知道樹是非線性的結構,如果我們把這些節點拍成一排,變成線性的一維結構,就要想辦法用一種順序來確定他們的關係。於是就有了先序遍歷、中序遍歷和後序遍歷。通過以上三種遍歷方式的其中一種,可以把整棵樹轉換成一維線性的結構輸出,通過以上三種便利方式的中序遍歷以及剩餘任意一種,可以將一維線性的這種結構還原成原本的非線性的樹的結構(關係)。
以上。
四、附件
學習途徑: b站郝斌數據結構
個人根據教程敲的一些代碼以及整理的一些筆記:百度雲鏈接【提取碼:sjeu】