本文由個人筆記整理得出,材料主要來自《大話數據結構》和網絡各博主
一、數據結構與算法的關係:
數據結構:一門研究非數值計算的程序問題中的操作對象,以及它們之間關係和操作等相關關係的學科。
說白了就是在一大堆數據中,數據元素之間的關係。(一個問題中多個對象之間的關係)
算法:解決特定問題求解步驟的描述,在計算機中表現爲指令的有限序列,並且,每條指令表示一個或多個操作。
說白了就是問題的具體解決方案唄。(特定問題的解決辦法)
所以,現在你明白了爲什麼要將數據結構和算法放在一起談了對吧?
這兩者是互相存在並且互爲對方服務的,不放在一起談根本都談不了。
二、兩者的一些概念:
數據結構的概念框架如下:
數據//由數據對象構成
|
數據對象//數據的子集
|
數據元素//組成數據的基本單位
|
數據項1 數據項2//不可分割的最小單位
數據結構是相互之間存在一種或多種特定關係的數據元素的集合。
根據視點的不同,我們把數據結構分爲邏輯結構和物理結構:
邏輯結構:集合結構、線性結構、樹形結構、圖形結構;
物理結構:順序存儲結構、鏈接存儲結構。
算法也有一些概念,具體的就不說了,這裏主要說一下算法的空間複雜度S(n)和時間複雜度T(n)。
時間複雜度T(n):在進行算法呢的時候,語句總的執行次數T(n)是關於問題規模n的函數,進而分析T(n)隨n的變化情況並確定T(n)的數量級。記作:T(n) = O(f(n)),表示隨着問題規模n的增大,算法執行時間的增長率和f(n)的增長率相同,稱作爲算法的漸近時間複雜度,簡稱時間複雜度。其中T(n)是問題規模n的某個函數,我們用O()作爲表現時間複雜度的記法。
直接上圖:
我們如何來理解這個O(n)函數呢?簡單來講,假如你在整一個程序中,一條語句在只執行一次,此時的時間複雜度爲O(1);
如:int listen = 100;//O(1)
listen= listen+listen;//O(2)
listen = listen+listen;//O(3)
printf(“listen:%d”,listen);//O(4)
但是,從更宏觀的程序執行流程視野來看,O(4)和O(1)是一樣的。爲什麼?因爲在數學裏面,O(1)和O(4)的階項都是一樣的,都等於1。所以整個算法的時間複雜度爲O(1)。
再舉例子如:for(i= 0;i<30;i++),那麼此時的時間複雜度爲O(30) = O(1);
假如用到的是一個循環語句:for(i= 0;i<n;i++),那麼此時的時間複雜度爲O(n);
爲什麼?我們可以從數學的角度上來看,n代表着無窮大,而4,30,100等等這些都是屬於同一個階項的,都等於1;無論你等於多少,只要是等於一個常數,那麼就是O(1)。
假如用的是兩個for嵌套在一起,那麼時間複雜度爲O(n的平方);假如這兩個for不是嵌套在一起,而是分開的一次執行的,那麼時間複雜度還是O(n)。畢竟你兩個O(n)在數量級上跟一個O(n)是一樣子的嘛,這個我相信對大家不難理解。以上的表格大家一次類推就好。
空間複雜度S(n):計算算法所需要的存儲空間,公式S(n) = O(f(n))//n爲問題的規模,f(n)爲關於n所佔存儲空間的函數。
舉個例子說,要判斷某年是不是閏年,你可能會花一點心思來寫一個算法,每給一個年份,就可以通過這個算法計算得到是否閏年的結果。
另外一種方法是,事先建立一個有2050個元素的數組,然後把所有的年份按下標的數字對應,如果是閏年,則此數組元素的值是1,如果不是元素的值則爲0。這樣,所謂的判斷某一年是否爲閏年就變成了查找這個數組某一個元素的值的問題。
第一種方法相比起第二種來說很明顯非常節省空間,但每一次查詢都需要經過一系列的計算才能知道是否爲閏年。第二種方法雖然需要在內存裏存儲2050個元素的數組,但是每次查詢只需要一次索引判斷即可。
這就是通過一筆空間上的開銷來換取計算時間開銷的小技巧。到底哪一種方法好?其實還是要看你用在什麼地方。
(例子來源鏈接:https://www.jianshu.com/p/88a1c8ed6254)
由此我們可以看出,算法的空間複雜度S(n)和時間複雜度T(n)本質上是有相互駁斥的,但在大多數情況下都優先考慮時間複雜度T(n),因爲算法的空間複雜度S(n)大頂多計算機多分配給它多一些內存而已;但是時間就不一樣了,有時候一個通過迭代的程序,可能跑了老半天!當然這個是相對而言的。
三、線性表:
線性表:零個或者多個數據元素的有限序列。
兩種物理結構:順序存儲結構(順序表)、鏈式存儲結構(鏈表:單鏈表、靜態鏈表、循環鏈表和雙想鏈表)。
順序表裏面元素的地址是連續的,鏈表裏面節點的地址不是連續的,是通過指針連起來的。
線性表的抽象數據類型定義如下:
ADT線性表(List)
Data
線性表的數據對象集合爲{a1,a2,a3......an};每一個元素的類型均爲DataType。其中,除第一個元素a1外,每一個元素有且只有一個直接前驅元素(也就說跟排隊一個,一個接一個),元素與元素之前是一對一關係。
Operation
InitList(*L)//初始化,建立一個空的線性表L
ListEmpty(L)//若鏈表爲空,返回true;否則返回false
ClearList(L)//清空線性表
GetElem(L,i,*e)//在線性表中L第i個元素值返回給e
LocateElem(L,e)//在線性表中查找與定值e相等的元素,如果有則返回1否則爲0
ListInsert(*L,i,e)//在線性表中L第i個元素插入e
ListDelete(*L,i,*e)//刪除線性表中L第i個元素,並用e返回其值
ListLength(L)//求線性表元素個數
Example順序表:
#define MAXSIZE 20//存儲空間初始化分配量
typedef int ElemType;
typedef struct
{
ElemType data[MAXSIZE];//數組存儲數據元素,最大值爲MAXSIZE
int length;//線性表當前長度
}SqList;
#define OK 1
#define ERROR 0
#define TRUE 1
#define FALSE 0
#define int Status
Status GetElem(SqList L,int i,ElemType *e)//獲得線性表元素
{
if(L.length == 0||i<1||i>L.length)//如果線性表長度爲0或者i>0(說明找不到這個元素)
return ERROR;
*e = L.data[i-1];
return OK ;
}
Status ListInsert(SqList *L,int i,ElemType e)//插入操作,在線性表中L第i個元素插入e
{
int k;
if(L->length = MAXSIZE)//線性表已經滿了
return ERROR;
if(i<1||i>L->length+1//在線性表中找不到
return ERROR;
if(i<=L->length)//想要插入的元素
{
for(k = L->length-1;k>=i-1;k--)//在第i個元素後的所有元素都要往後挪一位
L->data[k+1] = L->data[k];
}
L->data[i-1] = e;//直接把i插在最後一位
L->length++;
return OK ;
}
Status ListDelete(SqList *L,int i,ElemType e)//刪除線性表中L第i個元素,並用e返回其值
{
int k;
if(L->length = 0)//線性表爲空
return ERROR;
if(i<1||i>L->length+1//刪除選擇的位置不準確
return ERROR;
if(i<=L->length)//想要刪除的元素不是在表最後的位置
{
for(k = i;k<L_>length;k++)//在第i個元素後的所有元素都要往前挪一位
L->data[k-1] = L->data[k];
}
e= L->data[i-1] ;//把值返回給e
L->length--;
return e;
}
總結:
優點:可以快速的存取表中任意位置的元素
缺點:插入和刪除操作需要挪動大量元素、當表的長度變化比較大時,難以確定存儲空間的容量,容易造成存儲空間的“碎片”。
在順序表中,我們一般都是用數組來存儲數據的元素和利用數組的次序按安排元素之間的關係的。但在鏈式結構中,我們除了還有存儲數據元素外,還要存儲它的後繼元素的存儲地址,所以經常用到指針。
Example單鏈表的讀取,插入和刪除:
typedef struct Node
{
ElemType data;//數據
struct Node *next;//指針
}Node;
typedef struct Node *LinkList;
Status GetElem(LinkList L,int i,ElemType *e)//單鏈表的讀取,用e返回L中第i個元素的值
{
int j;
LinkList p;
p = L->next;//讓p指向鏈表L的第一個結點
j = 1;
while(p&&j<i)
{
p = p->next;
++j;
}
if(!p || j>i)//說明第i個結點不存在
return ERROR;
*e = p->data;//取i個結點的數據
return OK;
}
Status ListInsert(SqList *L,int i,ElemType e)//插入操作,在第i個結點之前插入新的元素e,L的長度+1
{
int j;
LinkList p,s;
p = *L;
j = 1;
while(p&&j<i)//尋找i-1結點
{
p = p->next;
++j;
}
if(!p || j>i)//說明第i個結點不存在
return ERROR;
s = (LinkList)malloc(sizeof(Node));//生成一個新的結點
s->data = e;
s->next = p->next;//
p->next = s;//將s賦值給了p的後繼
return OK;
}
Status ListDelete(SqList *L,int i,ElemType e)//刪除線性表中L第i個結點,用e返回其值,L的長度減1
{
int j;
LinkList p,q;
p = *L;
j = 1;
while(p->next&&j<i)//尋找i-1結點
{
p = p->next;
++j;
}
if(!p || j>i)//說明第i個結點不存在
return ERROR;
q = p->next;//生成一個新的結點
p->next = q-next;//把q指向下一個數據給了p-next
*e = q->data;
free(q);//用e返回其值
return e;
}
Example單鏈表的整表創建和刪除:
void CreateListHead(LinkList *L,int n)//整表創建表頭
{
LinkList p;
int i;
*L = (LinkList)malloc(sizeof(Node));//L爲整個線性表
(*L )->next = NULL;
for(i = 0;i<n;i++)
{
p= (LinkList)malloc(sizeof(Node));//生成頭結點
p ->data = rand()%100+1;//結點賦值
p->next = (*L)->next;//將新結點指向下一結點
(*L)->next = p;//插入到表頭
}
}
void CreateListTall(LinkList *L,int n)//整表創建其他元素
{
LinkList p,r;
int i;
*L = (LinkList)malloc(sizeof(Node));//L爲整個線性表
r = *L;//r爲指向尾部的結點
for(i = 0;i<n;i++)
{
p= (Node *)malloc(sizeof(Node));//生成新結點
p->data = rand()%100+1;//結點賦值
r->next= p ;//尾部節點指向新結點
r = p;//當前新結點定義爲表尾終端的結點
}
r->next = NULL;//當前鏈表結束,指向NULL
}
void ClearListTall(LinkList *L)//整表刪除
{
LinkList p,q;
p = (*L)->next;//p指向第一個結點
while(p)//當p不爲空時,也就是還沒到表尾最後一個結點
{
q = p->next;
free(p);
p = q;
}
(*L)->next = NULL;//當前頭指針指向NULL,意味着該表已空
return OK;
}
總結:
優點:在插入和刪除結點的時候比順序表有優勢,因爲有指針的指向,將指針指向下一個結點就行,不用對逐一對插入位置後面的元素進行操作;刪除也是這樣。
循環鏈表:
將單鏈表中終端結點的指針端改爲指向頭結點,使得整個單鏈表形成一個環。
雙向循環鏈表:
在單鏈表的每個結點中,再設置一個指向其前驅結點的指針域。
typedef struct DulNode
{
ElemType data;
struct DuLNode *prior;//前驅指針
struct DulNode *next;//後繼指針
}DulNode,*DuLinkList;
這裏只講一下怎麼插入:新結點s插入到p,p->next兩個之間
s->prior = p;//s 的前驅指向p
s->next = p->next;//s的後驅指向p-next
p->next ->prior = s;//把s賦值p->next的前驅
p->next = s;//把s賦值pt的後繼
靜態鏈表:
對於沒有指針的編程語言,可以用數組替代指針,來描述鏈表。
Example:
#define MAXSIZE 100
typedef struct
{
ElemType data;
int cur;
}Component,StaticLinkList[MAXSIZE];
Status InitList(StaticLinkList space)//鏈表初始化
{
int i;
for(i = 0;i<MAXSIZE-1;i++)
{
space[i].cur = i+1;
}
space[MAXSIZE-1].cur = 0;//最後一個元素的cur用來存放第一個插入元素的下標,相當於頭結點
}
int Malloc_SLL(StaticLink space)//
{
int i = space[0].cur;
if(space[0].cur)
{
space[0].cur = space[i].cur;
}
return i;
}
Status ListInsert(StaticLinkList L,int i,ElemType e)//在L中第i個元素插入新的數據元素e
{
int j,k ,l;
k = MAXSIZE-1;//K爲最後一個元素的下標
if (i<1 || i>ListLength(L)+1)
return ERROR;
j = Malloc_SSL(L);//獲得空閒分量的下標
if(j)
{
L[j].data = e;//將數據賦值給此分量的data
for(l = 1;l<i-1;l++)
k = L[k].cur;
L[j].cur = L[k].cur;
L[k].cur = j;
return OK;
}
rerurn ERROR;
}