数据结构(王道计算机考研笔记)
一、數據結構概念:
對數據之間的關系的結構類型進行總結歸納。
學好這門課,讓我們成為信息革命的參與者。
名詞解析:
數據項:您申請一個微博賬號,其中姓名,性別這些就是數據項
組合項:您賬號的生日是由年月日組成,年月日就是組合項
數據元素:由n個數據項組成的集合
????????
?何為結構呢?
?
?
就你去設計一個數據庫的時候,你需要考慮的就是運用什么邏輯結構設計這個數據庫
比如你使用了線性結構,然后就要考慮線性結構的存儲結構是什么,有順序,鏈式,索引以及散列。
??考點:從邏輯上可以把數據結構分成線性結構和非線性結構
?
?
數據元素之間的物理關系是什么?
?
?
🌟考點:清楚存儲結構有兩大類,順序存儲與非順序(鏈式、索引、散列)
?
三大結構:
1、線性結構?
2、樹型結構?
3、圓型結構?
兩大操作:
查找操作?
排序操作?
課后習題
?
二、算法的基本概念
🌟考點:
算法的特性
1、輸入輸出:可以沒有輸入,至少一個輸出
2、有窮:有限的步驟完成
3、確定:無二義
4、可行:每一步可行
設計算法的要求
1、正確
2、可讀性
3、健壯
4、高時效低存儲
?
?三、算法的效率如何度量之時間復雜度
?
?
時間復雜度的計算方法是這樣的
把代碼行用數字標記,然后看每一行的代碼會執行幾次,最后把每一行執行的次數相加/
最后您可以把計算的這個程序的時間復雜度進行一個總結成一個公式并且化簡。
不同的問題公式不同。
您觀察以下式子,每個公后面的列對總體計算結構影響可以忽略不計。
甚至連n項前面的k值也可以忽略不計。
當n值趨近于無窮的時候,兩個同階的公式之比為常數。
加法規則:比如o^2+o,那么就保留o^2即可。
乘法規則:如果是兩個式子相乘,那么就都保留。
舉個例子,如果o^3+o^2log2n,這是加法規則,該保留哪位呢?
?這張圖需要背下來
?
?
?
?
算法的效率如何度量之空間復雜度
?
?
?
課后習題:
?
?
四、線性表的定義和基本操作
線性結構也可以默認是線性表
線性結構=(D,R,O)?D:數據集?R:關系/結構?O:操作
線性結構也可以看成是線性表,線性表包括數據之間的物理結構中的線性存儲、鏈式存儲、索引存儲、散列(暫時不要求掌握)
如果我們要設計若干個班長候選人選票信息,那么我們需要設計數據類型和存儲結構
現在我們來看第一種線性結構?順序表
ElemType 數據類型的意思?
SqList是什么?
這里靜態動態SeqList和SqList只是這個結構體的名字。在main函數中起一個聲明線性表的作用。
?
如果您需要調用malloc和free這兩個函數,您需要在代碼聲明#include <stdib.h>頭文件
這一段講解又看不明白了的話去b站p8 15:00分重新看
定義一個順序表,這個順序表的數據結構類型是int類型,*data這個指針指向順序表中的第一個元素
然后我們實現了一個函數initList用于初始化一個動態分配方式實現的
再增加一個increaseSize函數用于增加動態數組的長度
之后我們再在main函數中調用相關的操作
我們再看這段代碼,首先先在main函數中聲明一個順序表。
然后使用InitList來申請一片連續的存儲空間,注意此處malloc函數前要用int強制轉換成和開頭時候int *data同類型的數據。
L.data=(int *)malloc(InitSize*sizeof(int)); //這行是用malloc申請一片連續的存儲空間,這片存儲空間的大小是能存的下10個int類型數據的大小,然后malloc函數會返回一個指針,其類型要轉換成int型,然后把malloc返回的值賦給data。
sizeof:sizeof是C語言的一個操作符,類似于++、–等。sizeof能夠告訴我們編譯器為某一特定數據或者某一個類型的數據在內存中分配空間時分配的大小,大小以字節為單位
此時需要把順序表的長度設置為0 L.length=0; //因為是初始化表,所以這里長度為0
把順序表的最大容量設置為InitSize與L.data中保持一致
此時內存圖中的MaxSize為10,length為10,data為橙色部分
再看main函數,如果您想往順序表中再插入五個元素,而此時內存空間已滿。
就需要用IncreaseSize函數來動態增加數組的長度
函數內的len表示我要拓展的長度,我們在main函數中傳入值5 IncreaseSize(L,5);
第一句我們定義了一個指針p,把順序表的中的data里的值賦給p。也就是說p指針和data是指向同一個位置
再看下一句 malloc是申請一整片的內存空間,此時能存下目前有的容量MaxSize以及加上現在要擴展的長度len
因為此時malloc申請的內存空間是新的一片內存空間,此時這片內存空間里沒有任何數據,我們用data這個指針指向新的內存空間,然后再用for循環把之前內存空間里的數據挪過去
因為內存容量的值增加了五個,所以L.MaxSize=L.MaxSize+len
然后 free函數會把指針p指向的那一片的存儲空間給釋放掉
?
為班級30位同學的語數英成績,設計一個統計系統//首先先建一個學生信息表 typedef struct Students{char sno[11]; //里面的11是這個學號是11位,所以這里放11,因為char sname[20]; //這里考慮少數民族,所以這里字符數放20位int c;int e;int m;}//再定義順序表 #define M 35 //表的最大長度為35 {ElemType data[M]; //靜態表存儲int length; //定義表長,這里int后的變量名可隨便取}?
?
?
?
順序表的基本操作——插入
???????
在for循環時候,此時順序表當前的長度為5(因為有五個元素),如果長度大于你要插入的那個數據的位置,則順序表中的最后一位向下挪一位 j--
for循環里的第一句的意思是,把data[4]里的數據放到第五位data[5]的位置去。
這里注意!代碼里說挪到第五位,這里是位序的概念!當前序列表長度雖然為5,但是處于第五位(最后一位)的數據元素位序為4.
因為現在我們要在位序為3的位置插入數據元素3,所以跳出for循環之后寫L.data[i-1]=e;
?其實這個代碼還不夠健壯,因為如果ListInsert(L,9,3);
而此時這個表的長度還只有6位,這樣則插在了位序下標為8的地方,但是之前我們說過順序表的特性就是必須有前序和后序節點。這樣就會報錯。
此時我們可以加一個判斷 插入i的合法值在[1,length+1],如果再要在第九位插入數據,因為已經超出了合法范圍,所以后續結構都不應該再繼續。
再者,如果插入的數據合法,但是您的表此時內存已經滿了,這時也無法運行。
而此時您除了考慮會遇到的這些意外情況外,還要站在使用者的角度來看,作為使用者,他需要知道自己的數據是否插入成功,程序需要給到使用者一定的反饋。
所以這里用bool判斷
?
順序表的基本操作——刪除
SqList &L是指看你取的是哪個順序表,i是要刪除這個順序表中的第幾個
&e是一個引用型的參數,用這個參數把此次要刪除的數據元素返回。
然后您看main函數,因為上個插入操作時候遺留的表里遺留了六個數據元素
如果您現在想刪除這個數據元素的話,首先您要定義一個和你的順序表中存儲的這些數據元素同類型的一個變量,因為我們這個順序表中存的都是int型,所以int e=-1;這里的-1是我們自己隨便寫的一個初始值。寫這個意味著我們內存中會開辟一小片空間用于存放e這個變量相關的數據。所以我們給它設置了一個初始值。所以此時e這片內存空間里存放的內容是-1這個值。
接下來再調用刪除這個基本操作,要刪除L這個表中的第三個元素。然后此時把此次要刪除的那個元素用e這個變量給返回。
我們再判斷里先判斷里這個內存值是否合法,如果false,此時我們main函數中的if語句就已經調用失敗了。因為我們要刪的是第3個,是合法的,所以會執行e=L.data[i-1]; 這行代碼會把此次要刪除的數據元素的值復制到e這個變量所對應的內存區域當中
大家要注意:
首先我們定義的這個刪除操作,e這個變量它是引用型的,由于加了這個&引用符號,所以在ListDelete這個函數里的變量e和main函數里定義的那個變量e在內存中對應的是同一份數據,如果沒有加這個&引用符號,在最后輸出的時候就還是會輸出最開始定義的那個-1.
順序表的基本操作——查找
???????
如果您還想讓您的程序更加健壯一些,您可以在這里判斷一下i的值是否合法,與插入,刪除一樣不再展開
而如果此時您用動態表來查找,初始化順序表部分里的data其實是一個指針,這個指針指向了順序表中的第一個數據元素,存儲這個順序表所需要的內存空間是用malloc函數申請的一整片的連續空間。
雖然data是一個指針,但它依然可以用這種數組下標的方式
?我們分析一下在計算背后發生了些什么?
初始化順序表部分里的data變量其實是一個指針,這個data指針指向了malloc函數給它分配的一整片連續內存空間的起始地址,假設此時指針data指向的地址是2000,在下面這張圖里一個小格代表一個字節的大小。
注意這里如果一個ElemType占6B,如果您要查找第一個元素,那么從指針data指向的2000這個地址開始,然后您看這個return L.data[i-1],這里在下標就是從為0的數據開始的,但是它要占據6個字節。如果您這個i輸入值為2的話,那么就是從data[1]那里開始往后數6位。
我們在ElemType里定義的data指針,它所指向的數據類型是ElemType這種類型,所以如果你按照這種數組下標的方式來寫代碼的話,計算機在背后會根據你的這個data指針所指向的數據類型它所占用的空間大小來給你計算每一個數組下標它應該對應的是哪幾個字節的數據。
如果這里定義類型是int型,p起始指向的地址也是2000的話,而int型一個字節只占4B
?
能夠實現隨機存取的基礎在于順序表中所有的元素在內存表中都是連續存放的,并且這些元素的數據類型相同,也就是說每個數據元素所占的內存空間是一樣大的。
只需要知道一個順序表的起始地址,然后再知道每一個數據元素的大小,就可以立即找到第i個元素存放的位置。這就是按位查找。
?按值查找就是要找到線性表L當中有沒有哪個數據元素和我們傳入的這個參數e是相等的,如果能找到這樣的數據元素的話,那么就要返回這個數據元素的存放位置。
(tips:在c語言中,=是賦值,==是判斷)
在LocateElem函數里傳入一個參數e?
然后開始執行for循環,i=0的意思是從這個順序表最開始的那個元素依次往后開始檢索i++。
此處的i時數組下標
if語句L.data[i]==e依次判斷順序表中的各個數據元素和我們傳入的這個數據元素e是否相等
如果相等的話,返回這個元素的位序
?
?
?
首先我們回顧前十節課的內容,學習了順序表的基本操作與實現。
所以如果有問題是問物理存儲結構的話,那就分為順序表和鏈式。
線性鏈表
首先我們先學習單鏈表
?question1 什么是單鏈表?
在一片內存空間中存放元素,每一塊元素用一個結點存儲,該結點除了存放數據元素外,還要存儲指向下一個節點的指針。像這樣每個結點用一個指針指向下一個結點的表叫單鏈表。
?順序表和單鏈表的優劣點:
單鏈表的各個節點在物理上可以是離散存放的,所以當我們要拓展單鏈表的長度時,只需要在內存中隨便摳一小塊區域作為存放新結點的區域就可以,比如下列圖片中紅色圓圈區域。
所以說采用鏈式存儲的話,改變容量會很方便,但是采用這種方式,我們需要找到某一個位序的結點,只能從第一個元素開始檢索,直到找到我們想要的那個節點。所以這種單鏈表的方式不支持隨機存取。
?用代碼定義一個單鏈表
單鏈表是由一個一個的結點組成的,而一個結點當中它需要有一個空間是用于存放數據元素,還需要有另一片空間存放指向下一個結點的指針。?
所以我們定義一個struct類型的結構體用于表示一個結點。這個結點中有一個叫data的變量用于存放這個數據元素,我們把這個稱之為數據域。
另外我們需要定義一個指向下一個結點的指針,這個指針變量的名字叫next,我們把這個指針叫指針域。
?了解這個結構體的定義之后,想要往單鏈表增加一個新結點的話,這里我們可以malloc函數來申請一片存儲這個結點的空間,并且用指針p來接收malloc函數的返回值,讓它指向這個結點的起始地址。
之后就可以涉及一些代碼邏輯,把p結點插入到這個單鏈表當中,按照我們background color:yellow部分的代碼的寫法,以后當我們想要定義一個新的結點,或者定義一個想要指向結點的指針的時候,都得寫
struct LNode*p=(struct LNode*)malloc(sizeof(struct LNode));?但因為這么寫有點麻煩,每次都得帶上struct這個關鍵字。
教材里使用了typedef(c語言中的關鍵字———數據類型重命名)
這個關鍵字可以把數據類型重命名,用法很簡單,其實就是在你的代碼里先寫一個typedef關鍵字,后面跟一個你想要重命名的數據類型,再空一行寫入你想要給它取的另一個別名。
ps:struct也是一種數據類型 只不過此處是結構體數據類型
?
在教材中我們采用這種方式來寫,非常簡潔。它等價于下面那種先struct定義。
?這段其實等價于下面這行咱們最開始定義的LNode的數據類型,下面這兩句typedef表示您把它先重命名為LNode,并且用*LinkList來表示這是一個指向struct LNode的指針
我們要表示一個單鏈表,只需要聲明一個頭指針L,這個頭指針指向單鏈表的第一個結點。
由于各個結點是由LNode指針把它們一個一個連起來的,所以只要我們找到了第一個結點,我們就找到了整個單鏈表。
即然L這個指針指向了某一個結點,我們定義L可以像這樣來定義
定義指針
LNode * L; //聲明一個指向單鏈表第一個結點的指針
根據上面那句typedef struct LNode *LinkList的重命名,LNode * L這句還可以等價于
LinkList L;
LNode * L; ???????和LinkList L;這兩種聲明方式在效果上來看是一模一樣的,但是這樣寫LinkList L;代碼可讀性更高
這二者都是聲明指針,有什么區別呢?
舉個栗子,現在我們有一個基本操作叫GetElem,就是把L這個鏈表當中第i個結點給取出來,并且return p給返回。
在這行代碼中既使用了LNode *,又使用了LinkList L,雖然二者在本質上是等價的,但GetElem這個函數最終是要返回第i個結點,所以它要返回值的類型代碼中定義成了LNode *,這里LNode *想強調的是這是我返回的一個結點。
而括號中的LinkList L它想強調的其實是它是一個單鏈表
所以括號里其實可以用LNode *L的方式來定義L這個參數,但因為這里并不想強調L它是一個結點,而是一個單鏈表。因此才使用了這樣的命名方式。
現在我們來看怎么初始化單鏈表?
首先是不帶頭結點的單鏈表
step1 申明一個指向單鏈表的指針L,執行這句之后,內存中會開辟一小塊空間,用于存放頭指針L
再往后執行初始化函數,把L的值設為NULL來表示當前是一個空表,做這一步是為了防止之前這一塊空間有遺留的臟數據。在bool括號里傳入&L這個指針變量時,我們是傳入了LinkList L的引用,如果不加&這個符號,那么我們在bool這個函數里修改的這個L就是頭指針函數里的一個復制品。
對于這種不帶頭結點的單鏈表,判斷它是否為空的依據就是看它的頭指針L此時是不是為NULL。
或者寫的更簡潔一些
因為L==NULL直接返回的就過就是true or ?false
?然后是帶頭結點的單鏈表
step1 申請一個單鏈表
step2 在初始化單鏈表的函數中用malloc函數申請一片空間存下LNode這樣的結點,并且把malloc返回的地址賦給L,所以我們說頭指針式指向了這個節點
step3 需要把L這個指針指向的節點next這個指針域把它設為NULL,意思是這個頭節點是不存儲數據的,我們設這個頭節點是為了后面實現一些基本操作會更方便一些。
這種帶頭節點的單鏈表我們要判斷它是否為空的話,就判斷這個頭節點的next指針域是否為空
?
二者就是寫代碼是否更方便的區別。
如果不帶頭節點的話,那么頭指針指向的下一個節點,那個結點就是實際存放數據的結點。
而如果帶頭結點的話,頭指針它所指向的這個結點把它稱為頭結點,這個頭結點是不存放實際數據元素的,只有這個頭結點之后的下一個結點才會用于存放數據。
?
單鏈表的插入與刪除
?1、帶頭節點的插入
如果我們需要在表L中第i的位置插入元素e,我們需要找到第i-1個結點,并修改其next指針。
假如i=2,那么我們首先需要找到第一個結點,也就是a1,然后用malloc函數申請一個新的結點,再往這個新結點里存入元素e,然后再對其左右結點的指針進行修改,這時這個新結點就變成了第二個結點。
假如i=1,此時就能看到這種帶頭結點的好處,我們可以把頭結點看成是“第0個”結點,以上分析的處理邏輯同樣適用。
下面我們看完整代碼
解析:
ListInsert函數當中傳入了一個&L單鏈表,這個單鏈表是帶頭結點的,int i是指定了此次要插入的位置的位序,ElemType e是給出了新結點當中要存放的數據元素的意思
如果我們要在i為1的位置插入新元素的話
首先代碼先來一個判斷if(i<1),因為位序是從1開始的,如果i<1就不合法,表示插入失敗,則return false。
因為此時i=1,因此這里申明一個指針p,LNode *p
這個指針p指向了和L相同的位置(p = L),即指向了頭結點。
int j=0; //我們定義了一個變量j,j表示的是當前p指向的是第幾個結點,因為頭結點是第0個結點,所以此時j的值應該是0.
注意此時雖然我們杜撰了一個第0個結點,可是它不存數據,實際存放數據的是頭結點后面的那些結點們。而結點的編號都是從1開始的,所以我們再if條件里不允許i<1
我們進入while循環,這一步是循環找到第i-1個結點,因為i=1,所以不滿足j<1-1,則跳出循環,同時也跳過if(p==NULL)
進入LNode *s =這句,它會申請一個新的結點空間
s->data =e;??//把參數e存到新結點里
s->next=p->next; //讓s的指向的這個結點的next指針,讓它等于p結點的指針指向的這個位置
?p->next=s; //讓p結點的next指針指向新的結點s,也就是這樣。
以下是完整代碼:
而且由于i=1,代碼中while循環直接被跳過,所以這里時間復雜度為0(1)
注意s->next=p->next; 和 p->next=s;這兩句不能顛倒
如果i=3
我們再看代碼,這里會進入while循環,由于前面的操作,使得j的值為0,同時p也不為null,進入循環。
此時p等于p的next,即讓p指向下一個結點
? ? ? ???? ? ??
然后j的值就變成了1,由于此時j依然是小于2的,所以還要再來一次循環
p還要再指向下一個結點
此時j值為2,跳出循環,然后再往后和i=1時情況一樣?
在表尾的位置插入新結點:這種情況 while循環執行次數最多,所以時間復雜度最壞,這里的n指的是表的長度。
?
分析:
當i=6時
?當j=5時,就跳出循環執行if(p==NULL)了,而因為i的值太大,直接return false
按位序插入(帶頭結點) 平均時間復雜度:O(n)
?1、不帶頭節點的插入
分析:
當i=1時,進入if(i==1)判斷中,申請一個新的結點LNode *s=(LNode *)malloc(sizeof(LNode));
s->data=e; //把e存寫到里面
s->next=L; //讓新結點的next指針指向L所指向的這個結點
L=s; //最后需要修改頭指針L?指向這個新的結點,然后return true表示插入成功
?由此可見,插入/刪除第一個元素,如果是不帶頭結點的情況,需要更改頭指針L的指向
所以在這里應該能體會不帶頭結點寫起代碼來會更麻煩。?
如果i>1,和帶頭結點的代碼是一樣的,只不過int j變成了1,int j=1
?此后代碼默認是帶頭結點的寫法。
指定結點的后插操作 ?
給定一個結點,在這個結點之后插入一個數據元素e
LNode *s = (LNode *)malloc(sizeof)?首先先malloc一個結點
if (s==NULL) return false如果malloc執行的結果是返回的一個NULL的話,則說明此次內存分配失敗。
若沒有這些特殊情況的話,就和直接的操作邏輯順序一樣。
s->data=e;
s->next=p->next;
p->next=s;
還記得之前在插入操作的代碼嗎?其邏輯其實就是找到結點,然后在其后插。
?然后現在有了這個后插操作的函數,在while找到要插入的結點之后,直接調用即可。
return InsertNextNode(p,e);
指定結點的前插操作:
如果想要找到指定結點的前一個結點,找到頭結點即可知道全貌。
如果您想在a3的位置前插一個元素,那么我們找到頭指針之后依次遍歷各個結點,找到a3的前驅結點,之后再對a2進行后插操作
?如果不給你傳頭指針的話怎么辦呢?
看另一種思路
首先申請一個新的結點
然后s->next=p->next;
p->next=s;然后把這個結點作為p結點的后繼結點?
?我的結點不能跑路,但結點里的數據可以跑路?
?s->data=p->data;這句代碼會把p結點以前存放的數據元素x給復制到s中
p->data=e; //把新插到數據元素e把它放到p這個結點里
?這種也能實現效果,且時間復雜度是o(1)
?王道書里直接給了結點s,找不到p的前驅結點,就先把s這個結點連接到p之后
再申明一個temp變量,先保存一下p結點里的內容。再把s結點的內容復制到p結點里,也就是p中的x變成了e,最后再把temp的值復制到s里面。
至此就實現了結點的前插操作。
按位序刪除(帶頭結點)
?如果要刪除i=1的元素的話,要將指針指向刪除的結點之后的那個結點。并且還要用free函數把刪除的那個結點內存給釋放掉
如果i=4,則代表我們要刪除的是a4這個結點,前面代碼和插入時候邏輯一樣
然后我們再定義一個指針q: LNode *q=p->next; q指針指向了p結點的next,也就是指向了第i個結點。
e=q->data; 接下來會把q結點的這個數據元素復制到變量e里面,注意這個變量e要把此次刪除的結點的值給帶回到這個函數的調用那里?即
???????
?所以e這個參數是引用類型的
p->next=q->next //p的next要指向q的next,也就是指向NULL
?最后調用free函數把q結點給釋放掉
?由于我們需要用while來一次循環找到第i-1個結點,所以這個算法
?代碼全貌
刪除指定結點?
我們要刪除結點p,
LNode *q=p->next;?首先先申明一個結點q為p的后繼結點
我們把p的后繼結點的數據,把它復制到p結點的數據域中
????????? ??????????
?p->next=q->next; 然后再讓p結點的next指針指向q結點之后的位置,可能指向的是一個結點,也可能是指向NULL
?最后free(q)
?這種時間復雜度為1
思考?
?如果p就是最后一個結點的話,那么在和后繼結點交換數據域時就會出錯
?
?所以到往后我們會學習雙鏈表
?
單鏈表的查找
1、按位查找
按位查找會出現以下極端情況
1、i=0
如果i為0的話直接返回第一個頭結點
2、i=8(這種情況大于鏈表長度)
看代碼p=L; //p是指向了頭結點,j的值剛開始是0,第一輪while循環后,p指針從頭結點指向下一個結點。
然后逐步while循環最終p指針指向NULL,再有這種情況,你基本可以判定i值不合法。
這些邊界情況都是我們在開發中必須考慮到的,這讓我們的程序更有健壯性。
3、普通情況i=3
這個算法平均時間復雜度:O(n)這個數量級
平均指的是該程序的i值在合法范圍內取值內任何一個數字的概率都等可能的情況。
2、按值查找
ElemType e給你一個數據元素e,LinkLIst L看在這個單鏈表中有沒有哪個結點的值是為e的。
假設本例中ElemType的數據類型是int,如果e這個變量為8:e=8
LNode *p:首先讓一個p指針
L->next:指向頭結點的下一個結點
即指向第一個數據結點。
之后再進入while循環,此時p不等于NULL滿足條件,且p目前所指的這個結點的數據域的值不等于e的值。
p=p->next; ?此時p指針指向下一個結點。
在第二輪循環時候,第二個結點的數值正好為8,此時把p結點給返回。
該算法的平均時間復雜度:O(n)
?情況2 e=6,不能找到的情況
如果結果返回NULL,則說明該鏈表中沒有e=6的數據。
如果ElemType是更復雜的結構類型呢?例struct
自行百度兩個struct類型如何判斷其相等。
?該算法時間復雜度:O(n)
思考一下不帶頭結點呢?
? ? 單鏈表的建立
?
step1第一步在前面幾節課已經講過
1、尾插法建立單鏈表?
由于我們每次都要把數據元素插入到單鏈表的表尾,所以我們可以設置一個變量length記錄鏈表長度,再寫一個while循環,每次取出一個數據元素e,然后調用位序插入ListInsert這個基本操作,每一個都把數據元素e都插入到第length+1的位置,像下面這個例子,length+1等于4,也就是插入到紅色箭頭的位置,也就是表尾的位置,每次插入一個新的元素之后,都會導致單鏈表的長度length+1,這樣的方式就可以實現用尾插法建立一個單鏈表
?用這種方式實現的話,當你每次要在表尾的位置插入一個元素的時候,它都會用這個while循環從表頭的位置開始依次往后遍歷,直到找到最后一個結點,按照這個邏輯,當我們要插入第一個元素的時候,也就是只有一個頭結點的時候,這個while循環可以直接跳過,循環次數是0次,我們要插入第二個元素的時候,while循環一次,插入第三個元素的時候,while循環兩次,以此類推。所以我們要插入n個元素的話,總共需要循環n-1次,算出來時間復雜度是O(n^2)
這個時間復雜度很高,而且我們沒必要每次都從頭開始找。
解決方案:
設置一個表尾指針r,之后再在尾部插入結點,則對尾部進行一個后插操作即可。
int x;首先聲明了一個局部變量x
L=(LinkList)malloc(sizeof(LNode)));然后用malloc函數申請了一個頭結點,也就是說這里面做了初始化一個單鏈表的操作,只不過我們自己初始化一個單鏈表的時候會把頭結點的指針先把它設為NULL,而該代碼沒有這樣操作,因為頭結點的指針會在后面被修改。
LNode *s,*r=L; //申明了兩個指針,這兩個指針都指向了頭結點。
scanf("%d",&x); //讓用戶從鍵盤里輸入一個整數,這個整數x就是此次要單鏈表中的數據元素
while(x!9999)這里隨便寫了個9999,如果用戶輸入該數,這里就結束。
我們輸入數字10,因此進入循環
s=(LNode*)malloc(sizeof(LNode)); //malloc申請一個新的結點,讓s這個指針指向新結點
s->data=x; //并且把新結點的這個數值設為x,也就是此次輸入的這個數字。
r->next=s; //把r結點的next指針指向s這個結點。
r=s; //最后再讓r指針指向s這個結點
????????
?假設我們下一個數輸入數字16,則繼續執行while循環,設置一個新結點
? ? ? ? ? ? ? ? ? ???
?如果輸入9999,則跳過循環
r->next=NULL; ?//讓r結點的next指向NULL
return L; //最后再調用返回這個紅色圓圈的頭指針L
頭插法建立單鏈表
雙鏈表
為什么要要使用雙鏈表:
-
單鏈表:無法逆向檢索,有時候不太方便
-
雙鏈表:可進可退,但是存儲密度更低一丟丟
雙鏈表就是在單鏈表的基礎上再增加一個指針域prior,屬于前驅結點。
雙鏈表中的結點我們把它命名為DNode。(D指的就是double)
雙鏈表的初始化(帶頭結點):
void{
DLinklist L //申明了一個指向頭結點的指針L
IinitDLinkList(L); //調用雙鏈表的初始化函數
}
InitDLinkList{
L=(DNode *)malloc(sizeof(DNode)); //申請一片內存空間用來存放頭結點,并且讓L指針指向這個頭結點
「 L-prior=NULL;?L->next=NULL;」//前后指針都設為NULL
}
然后您再看typedef里聲明鏈表,和單鏈表類似。
DNode *和DLinklist等價,而您反觀bool函數和void有的方法類似,有的地方使用DLinklist L是想強調它是一個鏈表,而有的地方使用DNode *是想強調是一個結點。
如果您想要判斷雙鏈表是否為空,您就判斷頭結點的next指針是否為NULL就可以了
如果為NULL,說明此時鏈表暫時沒有存入任何數據元素
雙鏈表的插入:
step1 把s結點的next指針指向p結點的下一個結點 (s指向橙色方塊y)
step2 把p結點的后繼結點,它的前項指針指向此次新插入的s這個結點 (橙塊y指向s)
step3 把s結點的前項指針指向p結點
step4 把p結點的后繼指針指向s結點
-
小心如果p結點為最后一個結點產生的空指針問題,因此循環鏈表應運而生(詳見后面的循環鏈表插入刪除)
-
注意指針的修改順序
雙鏈表的刪除:
???????
雙鏈表的遍歷:
循環鏈表
循環單鏈表與單鏈表的區別:
單鏈表:
-
表尾結點的next指針指向NULL
-
從一個結點出發只能找到后續的各個結點
循環單鏈表:
-
表尾結點的next指針指向頭結點
-
從一個結點出發可以找到其他任何一個結點
循環單鏈表初始化:
循環雙鏈表:
循環雙鏈表的初始化:
-
從頭結點找到尾部,時間復雜度為O(n)
-
如果需要頻繁的訪問表頭、表尾,可以讓L指向表尾元素(插入、刪除時可能需要修改L)
-
從尾部找到頭部,時間復雜度為O(1)
循環雙鏈表與雙鏈表的區別:
雙鏈表:
-
表頭結點的prior指向NULL
-
表尾結點的next指向NULL
-
表頭結點的prior指向表尾結點
-
表尾結點的next指向頭結點
-
循環鏈表的插入:
?
注意點:
寫代碼時候注意以下幾點,以此規避錯誤:
如何判空
如何判斷結點p是否是表尾/表頭元素(后向/前向遍歷的實現核心)
如何在表頭、表中、表尾插入/刪除一個結點
靜態鏈表
什么是靜態鏈表:
分配一整片連續的內存空間,各個結點集中安置
每個結點由兩部分組成:data(數據元素)和next(游標)
0號結點充當“頭結點”,不具體存放數據
游標為-1表示已經到達表尾
游標充當“指針”,表示下個結點的存放位置,下面舉一個例子:
每個數據元素4B,每個游標4B(每個結點共8B),設起始地址為addr,e1的存放地址為addr + 8*2(游標值)
定義靜態鏈表:
-
方法2:
基本操作:
初始化:
把a[0]的next設為-1
把其他結點的next設為一個特殊值用來表示結點空閑,如-2
插入位序為i的結點:
找到一個空的結點,存入數據元素(設為一個特殊值用來表示結點空閑,如-2)
從頭結點出發找到位序為i-1的結點
修改新結點的next
修改i-1號結點的next
刪除某個結點:
從頭結點出發找到前驅結點
修改前驅結點的游標
被刪除結點next設為-2
總結:
靜態鏈表:用數組的方式實現的鏈表
優點:增、刪操作不需要大量移動元素
缺點:不能隨機存取,只能從頭結點開始依次往后查找;容量固定不可變
適用場景:①不支持指針的低級語言;②數據元素數量固定不變的場景(如操作系統的文件分配表FAT)
順序表和鏈表的比較
邏輯結構:
都屬于線性表,都是線性結構
存儲結構:
順序表:
優點:支持隨機存取、存儲密度高
缺點:大片連續空間分配不方便,改變容量不方便
鏈表:
優點:離散的小空間分配方便,改變容量方便
缺點:不可隨機存取,存儲密度低
基本操作:
順序表:
創建
需要預分配大片連續空間。
若分配空間過小,則之后不方便拓展容量;
若分配空間過大,則浪費內存資源
靜態分配:靜態數組實現,容量不可改變
動態分配:動態數組(malloc、free)實現,容量可以改變但需要移動大量元素,時間代價高
銷毀
修改Length = 0
靜態分配:靜態數組,系統自動回收空間
動態分配:動態數組(malloc、free),需要手動free
增刪
插入/刪除元素要將后續元素都后移/前移
時間復雜度O(n),時間開銷主要來自移動元素
若數據元素很大,則移動的時間代價很高
查
按位查找:O(1)
按值查找:O(n)若表內元素有序,可在O(log2n)時間內找到
鏈表:
創建
只需分配一個頭結點(也可以不要頭結點,只聲明一個頭指針),之后方便拓展
銷毀
依次刪除各個結點(free)
增刪
插入/刪除元素只需修改指針即可
時間復雜度O(n),時間開銷主要來自查找目標元素
查找元素的時間代價更低
查
按位查找:O(n)
按值查找:O(n)
用哪個:
表長難以預估、經常要增加/刪除元素——鏈表
表長可預估、查詢(搜索)操作較多——順序表
開放式問題的解題思路:
問題: 請描述順序表和鏈表的bla bla bla…實現線性表時,用順序表還是鏈表好?
答案:
順序表和鏈表的邏輯結構都是線性結構,都屬于線性表。
但是二者的存儲結構不同,順序表采用順序存儲…(特點,帶來的優點缺點);鏈表采用鏈式存儲…(特點、導致的優缺點)。
由于采用不同的存儲方式實現,因此基本操作的實現效率也不同。
當初始化時…;當插入一個數據元素時…;當刪除一個數據元素時…;當查找一個數據元素
?
是
總結
以上是生活随笔為你收集整理的数据结构(王道计算机考研笔记)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 《HeadFirst Python》第一
- 下一篇: 典型的 C++ 程序员成长经历