相信很多學(xué)習(xí)C語言、Java等編程語言的小伙伴們在掌握了基礎(chǔ)語法后就了解到了數(shù)據(jù)結(jié)構(gòu)與算法,這兩個學(xué)科熬禿了多少程序員的頭。數(shù)據(jù)結(jié)構(gòu)和算法的關(guān)系是依賴的,實現(xiàn)算法需要一定的數(shù)據(jù)結(jié)構(gòu),數(shù)據(jù)結(jié)構(gòu)有很多種類,其中最簡單的一種就是線性表,而線性表中又分為順序表和鏈式表(簡稱鏈表),我們就來介紹一下線性表的這兩種表。
基礎(chǔ)數(shù)據(jù)類型
我們知道,每一門語言都有一些基礎(chǔ)的數(shù)據(jù)類型,比如int,float,char,這些數(shù)據(jù)類型就像一個一個的點。但是我們在實際使用的時候會把一些有關(guān)聯(lián)的點組合起來使用,這就是有結(jié)構(gòu)的數(shù)據(jù)(比如將一個一個的數(shù)據(jù)存放在一起,這就是一個集合(枚舉類型))。
線性表
數(shù)據(jù)結(jié)構(gòu)不僅描述了數(shù)據(jù)的格式,還描述了數(shù)據(jù)與數(shù)據(jù)間的關(guān)系,最常見的就是數(shù)據(jù)與數(shù)據(jù)之間一一聯(lián)系,前一個數(shù)據(jù)和后一個數(shù)據(jù)相連,這種數(shù)據(jù)結(jié)構(gòu)最后會像線一樣連成一串,這種數(shù)據(jù)結(jié)構(gòu)也因此得名為線性表。
線性表的實現(xiàn)有兩種形式,這兩種實現(xiàn)方式的不同主要是內(nèi)存造成的:
順序表
實現(xiàn)線性表的最簡單的方式,就是在一片連續(xù)的內(nèi)存中按照順序填充數(shù)據(jù),這樣每個數(shù)據(jù)都會像排隊一樣在內(nèi)存空間里有一定的位置。要訪問數(shù)據(jù)的前一個數(shù)據(jù),只要在內(nèi)存中相應(yīng)偏移一定的量(通常是一個數(shù)據(jù)的長度),就能訪問到相應(yīng)的數(shù)據(jù),訪問后一個數(shù)據(jù)也可以使用相同的方式。
實際上在看到連續(xù)的內(nèi)存這個字眼,小伙伴們應(yīng)該馬上就想到了吧?沒錯,就是數(shù)組(在python中沒有數(shù)組,但有隊列和元組,在此處對應(yīng)的是元組,下文會有所介紹)
null | arr0=W | arr1=3 | arr2=C | arr3=S | arr4=C | arr5=H | arr6=o | arr7=o | arr8=l |
null | null | null | null | null | null | null | null | null | null |
如上所示,這就是一個典型的順序表。
順序表的問題
前文中我們提到,創(chuàng)建順序表需要開辟一塊固定的空間,通常這個空間開辟后就無法修改其容量,也就是所這個順序表初始化的時候只能存放10個數(shù)據(jù),就不能存放超過十個數(shù)據(jù)。這樣的使用會存在這樣的問題:我怎么知道我需要多大的空間,換言之,在不知道需要多大空間的時候,使用順序表就要根據(jù)經(jīng)驗來判斷了,如果預(yù)先開辟過大的空間,就會有空間浪費的問題。另外,順序表的數(shù)據(jù)操作也有一定的資源浪費,修改數(shù)據(jù)上邏輯是正常的,我只要到固定的位置改動數(shù)據(jù)即可,刪除和添加最后一個數(shù)據(jù)也是正常的,還是到最后的位置處理數(shù)據(jù)即可,那么我要在順序表中間或者開頭插入一個數(shù)據(jù)呢?這個時候我就要在要插入的位置開始,將所有的數(shù)據(jù)后移一位,才能將新數(shù)據(jù)填充進去,刪除也有同樣的情況,要將要刪除的
此外還有另一個問題:如果我本身沒有這么多空間呢?內(nèi)存的使用并不是有規(guī)律的,開辟一塊固定寬度的空間很多時候只是一種奢求,如何在支零破碎的內(nèi)存中利用好每個空隙,這就是接下來的主角——鏈式表的優(yōu)勢了。
鏈式表
與順序表不同,順序表是將一堆數(shù)據(jù)捆在一起,使用物理相鄰的方式來確定他們的關(guān)系。而鏈式表這是數(shù)據(jù)與數(shù)據(jù)之間各自在其內(nèi)存空間,不過數(shù)據(jù)中保存著訪問下一個(或者上一個)數(shù)據(jù)的方法。如何通俗的理解這個方法呢?假如一群朋友去看電影,但是沒有辦法買到連坐的位置,想要知道某個人的位置,你可以問旁邊的你的朋友,因為你知道你旁邊的你的朋友在哪,而你的朋友又可以問你的另一個朋友,知道找到那個朋友。如下所示:
arr1 =W
下一個目標是 第三格 |
null | arr2 =3
下一個目標是 第五格 |
null | arr3 =C
下一個目標是 第六格 |
arr4=/0
這里是鏈表尾部 |
null |
在C語言中使用指針來保存下一個(或上一個)數(shù)據(jù)的數(shù)據(jù)地址,在其他語言中使用引用來保存下一個(或者上一個)的數(shù)據(jù)(因為是引用,所以實際上也是保存了一個數(shù)據(jù)地址),如下所示:
一個node就是一個數(shù)據(jù),也就是一個數(shù)據(jù)節(jié)點,每個節(jié)點由數(shù)據(jù)域和指針域構(gòu)成,數(shù)據(jù)域用來存放數(shù)據(jù),而指針域用來指定下一個目標節(jié)點。
由于鏈表的這種特性,要在中間插入一個數(shù)據(jù),只需要新建一個數(shù)據(jù)節(jié)點,然后將前一個數(shù)據(jù)的指針域指向這個新數(shù)據(jù),然后將這個新數(shù)據(jù)指向后一個指針域即可。刪除也很簡單,我們只需要將前一個數(shù)據(jù)的指針域指向下一個數(shù)據(jù),然后刪除這個數(shù)據(jù)即可(在c語言中線性表需要自行實現(xiàn),或者使用相應(yīng)的庫,而在python中列表具有相應(yīng)的功能,但列表并不只是單純的鏈式表,你可以看做他是鏈式表的升級版或者超集)。
要想知道一個鏈表有多少個數(shù)據(jù)節(jié)點,只有將所有的數(shù)據(jù)遍歷過才能知道,當然,你可以選擇用一個數(shù)據(jù)節(jié)點來保存這個鏈表的長度,但是你需要訪問最后一個節(jié)點,假如這個鏈表的長度為十萬個,那么就需要執(zhí)行十萬次數(shù)據(jù)讀取才能得到這個節(jié)點的數(shù)據(jù),而順序表只需要一次(他可以直接通過內(nèi)存偏移得到相應(yīng)的數(shù)據(jù))
鏈表的C語言實現(xiàn):單鏈表
#include <stdio.h>
#include <stdlib.h>
typedef struct Link{
int elem;
struct Link *next;
}link;
link * initLink();
//鏈表插入的函數(shù),p是鏈表,elem是插入的結(jié)點的數(shù)據(jù)域,add是插入的位置
link * insertElem(link * p,int elem,int add);
//刪除結(jié)點的函數(shù),p代表操作鏈表,add代表刪除節(jié)點的位置
link * delElem(link * p,int add);
//查找結(jié)點的函數(shù),elem為目標結(jié)點的數(shù)據(jù)域的值
int selectElem(link * p,int elem);
//更新結(jié)點的函數(shù),newElem為新的數(shù)據(jù)域的值
link *amendElem(link * p,int add,int newElem);
void display(link *p);
int main() {
//初始化鏈表(1,2,3,4)
printf("初始化鏈表為:\n");
link *p=initLink();
display(p);
printf("在第4的位置插入元素5:\n");
p=insertElem(p, 5, 4);
display(p);
printf("刪除元素3:\n");
p=delElem(p, 3);
display(p);
printf("查找元素2的位置為:\n");
int address=selectElem(p, 2);
if (address==-1) {
printf("沒有該元素");
}else{
printf("元素2的位置為:%d\n",address);
}
printf("更改第3的位置的數(shù)據(jù)為7:\n");
p=amendElem(p, 3, 7);
display(p);
return 0;
}
link * initLink(){
link * p=(link*)malloc(sizeof(link));//創(chuàng)建一個頭結(jié)點
link * temp=p;//聲明一個指針指向頭結(jié)點,用于遍歷鏈表
//生成鏈表
for (int i=1; i<5; i++) {
link *a=(link*)malloc(sizeof(link));
a->elem=i;
a->next=NULL;
temp->next=a;
temp=temp->next;
}
return p;
}
link * insertElem(link * p,int elem,int add){
link * temp=p;//創(chuàng)建臨時結(jié)點temp
//首先找到要插入位置的上一個結(jié)點
for (int i=1; i<add; i++) {
if (temp==NULL) {
printf("插入位置無效\n");
return p;
}
temp=temp->next;
}
//創(chuàng)建插入結(jié)點c
link * c=(link*)malloc(sizeof(link));
c->elem=elem;
//向鏈表中插入結(jié)點
c->next=temp->next;
temp->next=c;
return p;
}
link * delElem(link * p,int add){
link * temp=p;
//遍歷到被刪除結(jié)點的上一個結(jié)點
for (int i=1; i<add; i++) {
temp=temp->next;
}
link * del=temp->next;//單獨設(shè)置一個指針指向被刪除結(jié)點,以防丟失
temp->next=temp->next->next;//刪除某個結(jié)點的方法就是更改前一個結(jié)點的指針域
free(del);//手動釋放該結(jié)點,防止內(nèi)存泄漏
return p;
}
int selectElem(link * p,int elem){
link * t=p;
int i=1;
while (t->next) {
t=t->next;
if (t->elem==elem) {
return i;
}
i++;
}
return -1;
}
link *amendElem(link * p,int add,int newElem){
link * temp=p;
temp=temp->next;//tamp指向首元結(jié)點
//temp指向被刪除結(jié)點
for (int i=1; i<add; i++) {
temp=temp->next;
}
temp->elem=newElem;
return p;
}
void display(link *p){
link* temp=p;//將temp指針重新指向頭結(jié)點
//只要temp指針指向的結(jié)點的next不是Null,就執(zhí)行輸出語句。
while (temp->next) {
temp=temp->next;
printf("%d",temp->elem);
}
printf("\n");
}
回到上文的問題
為什么在python中順序表對應(yīng)的是元組而不是列表?原因很簡單,python的列表是可以從中間插入數(shù)據(jù)的,而這是鏈式表的特性,元組不能實現(xiàn)這個功能。
小結(jié):順序表和鏈式表的優(yōu)缺點比較
順序表 | 鏈式表 | |
內(nèi)存使用 | 連續(xù)的一塊空間,數(shù)據(jù)相鄰,通過物理相鄰的方式實現(xiàn)數(shù)據(jù)之間的聯(lián)系 。
因此不能動態(tài)調(diào)整長度 (內(nèi)存使用率較低) |
每個數(shù)據(jù)節(jié)點不一定相鄰,通過指針的方式實現(xiàn)數(shù)據(jù)之間的聯(lián)系
因此可以調(diào)整動態(tài)長度 |
刪除 | 可以刪除最后一個數(shù)據(jù),刪除其他位置的數(shù)據(jù)的時候需要將相應(yīng)的數(shù)據(jù)前移
(刪除成本高) |
數(shù)據(jù)可以隨意刪除,只需要將前一個節(jié)點的指針指向后一個節(jié)點 |
插入 | 可以在順序表最后添加一個新的數(shù)據(jù),在其他位置插入數(shù)據(jù)的時候需要將相應(yīng)的數(shù)據(jù)后移再插入
(插入成本高) |
數(shù)據(jù)可以隨意插入,只需要將前一個節(jié)點的指針指向新的節(jié)點,再將新的節(jié)點的指針指向下一個節(jié)點 |
元素查詢 | 索引與內(nèi)存偏移正相關(guān),只需要知道索引就可以得到相應(yīng)的內(nèi)存地址,只需要執(zhí)行一次數(shù)據(jù)訪問就可以得到數(shù)據(jù) | 需要一個節(jié)點一個節(jié)點的查找才能得到數(shù)據(jù),當節(jié)點在末尾的時候需要遍歷整個鏈表才能得到數(shù)據(jù)。
(查詢成本高) |