算法:解決特定問題求解步驟的描述
好的算法:時間效率高和存儲量低
判斷一個算法的效率時,函數中的常數和其他次要項常常可以忽略,而更應該關注最高階項的階數:輸入規模越大,次要項影響越來越小
算法事件複雜度:隨着問題規模n的增大,算法執行時間的增長率和f(n)的增長率相同;O(n) ;(n就是程序語句執行多少次)
對數階:2的x次方等n,x=O(logn)
int count = 1;
while(count < n)
count = count * 2;
順序表,在存、讀數據時,不管哪個位置,時間複雜度都是O(1);而插入或刪除時,時間複雜度都是O(n)
鏈表的每個節點中只包含一個指針域:單鏈表
頭指針:鏈表指向第一個節點的指針,若鏈表有頭節點,則是指向頭節點的指針;無論鏈表是否爲空,頭指針均不爲空;頭指針是鏈表的必要元素
頭結點:不一定是鏈表的必須要素;爲了操作的統一和方便而設立的
typedef struct Node{
ElemType data;
struct Node* next;
}Node;
typedef struct Node* LinkList;
單鏈表的讀取(第i個數據)
鏈表插入:注意要重新申請一個節點的內存空間
鏈表刪除:主要要釋放刪除節點的內存
頭插法:p =(LinkList*)malloc(sizeof(Node)); p -> next = head->next;
head -> next = p;
尾插法: p = (LinkList*) malloc(sizeof(Node)); head -> next = p; head = p;
整表刪除:p = head->next; while(p) { q = p->next; free(p); p = q;}
單鏈表和順序表:
- 存儲方式:用一段連續的存儲單元依次存儲線性表的數據元素;單鏈表用一組任意的存儲單元存放線性表的元素
- 時間性能:查找:O(1)、O(n);插入和刪除:O(n)、O(1)
- 空間性能:順序存儲結構需要預分配空間
靜態鏈表:用數組描述的鏈表;數組的元素由兩個數據域組成:data存放數據元素,cur存放該元素的後繼在數組中的下表
#define MAX_SIZE 1000
typedef struct
{
ElemType data;
int cur;
}Component,StaticLinkList[MAXSIZE];
數組第一個元素,即下標爲0的元素的cur就存放數組空閒空間的第一個元素的下標
數組最後一個元素的cur存放第一個有數值的元素的下表(相當於頭結點)
循環鏈表:將單鏈表中終端節點指針由空指針改爲指向頭節點,就使整個單鏈表形成一個環,這種頭尾銜接的單鏈表稱爲循環鏈表
循環鏈表和單鏈表的主要差異就在於循環的判斷條件上,原來是判斷p->next 是否爲空,現在則是p->next不等於頭結點,則循環未結束
單鏈表有了頭節點,我們可以用O(1)的時間訪問第一個節點,但對於要訪問最後一個節點,卻需要O(n)時間
我們用指向終端結點的尾指針來表示循環鏈表,此時查找開始節點和終端節點都很方便了:終端節點用read指針指示,查找終端節點:rear;查找開始節點rear->next->next
將兩個鏈表合併成一個表時,有了尾指針也非常簡單:已知rearA和rearB
p = rearA->next;
rearA ->next = rearB->next->next;
rearB->next = p;
free(rearB->next);
雙向鏈表:在單鏈表的每個節點中,再設置一個指向其前驅節點的指針域
單鏈表根據next指針要查找上一節點的話,最壞的時間複雜度就是O(n),因爲每次都要從頭開始遍歷查找
插入操作:將s節點插入到節點p和p->next之間
s->prior = p;
s->next = p->next;
p->next ->prior = s;
p->next = s;
刪除操作:刪除節點p
p->prior->next = p->next;
p->next->prior = p->prior;
棧:限定僅在表尾進行插入和刪除操作的線性表;棧頂和棧底;後進先出
棧的順序存儲結構(需要事先確定數組存儲空間大小)
- 數組下標爲0的一端作爲棧底;
- top棧頂元素在數組中的位置,top必須小於StackSize
空棧:top = -1
typedef int SElemType; typedef struct { SElemType data[MAXSIZE]; int top; //用於棧頂指針 }SqStack; Status Push(SqStack* s,SElemType e) { if(s->top == MAXSIZE-1) return ERROR; s->top++; s->data[s->top] = e; return OK; } Status Pop(SqStack* s, SElemType e) { if(s->top == -1) return ERROR; e = s->data[s->top]; s->top--; return OK; }
棧的鏈式存儲結構:棧頂指針就是指向鏈表的頭部(第一個節點),對於鏈棧來說,是不需要頭節點的。
typedef struct StackNode
{
ElemType data;
struct StackNode* next;
}StackNode,*LinkStackPtr;
typedef struct LinkStack
{
LinkStackPtr top;
int count;
}LinkStack;(鏈棧)
Status Push(LinkStack* s,SElemType e)
{
LinkStackPtr s = (LinkStackPtr)malloc(sizeof(StackNode));
s->data = e;
s->next = s->top;
s->top = s;
s->count++;
return OK;
}
Status Pop(LinkStack* s,SElemType* e)
{
LinkStackPtr p;
if(StackEmpty(*s))
return ERROR;
*e = s->top->data;
p = s->top;
s->top = s->top->next;
free(p);
s->count--;
return OK;
}
如果棧的使用過程元素變化不可預料,有時大有時小,那麼最好用鏈棧;反之,棧的大小變化範圍在可控範圍內,建議使用順序棧
棧的應用–遞歸:在前行階段,對於每一層遞歸,函數的局部變量、參數值以及返回地址都被壓入棧中,在退回階段,位於棧頂的局部變量、參數值和返回地址被彈出,恢復調用的狀態
棧的應用–四則運算表達式:
- 後綴表達式:所有的符號都是在要運算數字的後面出現
- “9+(3-1)X3+10/2”:“931-3*+102/+”
- 規則:從左到右遍歷表達式的每個數字和符號,遇到數字就進棧,遇到符號,就將處於棧頂兩個數字出棧,進行運算,運算結果進棧,已知到最終獲得結果
中綴表達式轉後綴表達式:從左到右遍歷中綴表達式的每個數字和符號,若是數字就輸出,即稱爲後綴表達式的一部分;若是符號,則判斷其與棧頂符號的優先級,是右括號或優先級低於棧頂符號,則棧頂元素依次出棧並輸出
隊列:先進先出;只允許在一端進行插入操作,而在另一端進行刪除操作的線性表
爲了避免當只有一個元素時,隊頭和隊尾重合使處理變得麻煩,引入兩個指針,front指針指向隊頭元素,rear指針指向隊尾元素的下一個位置;當front等於rear時,空隊列
循環隊列:隊列的頭尾相接的順序存儲結構
typedef struct
{
QElemType data[MAXSIZE];
int front;
int rear;
}SqQueue;
隊列的鏈式存儲結構:其實就是單鏈表,只不過是尾進頭出
隊頭指針指向鏈隊列的頭結點,隊尾指針指向終端節點
typedef struct QNode { QElemType data; struct QNode* next; }QNode,*QueuePtr;
typedef struct
{
QueuePtr front, rear;
}LinkQueue;
串:由零個或多個字符組成的有限序列
查找子串位置
int Index(String s,String t,int pos)
{
int n = strlen(s);
int m = strlen(t);
int i = pos;
String sub;
while(i <= n-m+1)
{
SubStr(sub,s,i,m);
if(StrCompare(sub,t) != 0)
{
++i;
}
else
{
return i;
}
}
return 0;
}
串的模式匹配
從主串“goodgoogle”中找到”google”這個子串的位置:對主串的每一個字符作爲子串的開頭,與要匹配的字符串進行匹配
int Index(String s,String t,int pos)
{
int n = strlen(s);
int m = strlen(t);
int i = pos;
int j = 0;
while(i < n && j < m)
{
if(s[i] == t[j])
{
i++;
j++;
}
else
{
i = i-j+1;
j = 0;
}
}
if(j>m)
return i-m;
else
return 0;
}
樹:n個節點的有限集;n=0時稱爲空樹;
在任意一顆非空樹中:
- 有且僅有一個特定的稱爲根的節點
當n>1時,其餘節點可分爲m個互不相交的有限集T1, T2, T3…..Tm,其中每一個集合本身又是一棵樹,並稱爲根的子樹
節點擁有的子樹數稱爲節點的度;度爲0的節點稱爲葉節點;樹的度就是樹內各節點的度的最大值
- 節點的祖先是從根到該節點所經分支上的所有節點。
- 樹中節點的最大層次稱爲樹的深度或高度
如果將樹中節點的各子樹看成從左到右是有次序的,不能互換的,則稱該樹爲有序樹
雙親表示法:每個節點:data(數據域)和parent(指針域:存儲該節點的雙親在數組中的下標)
孩子表示法:每個節點有多個指針域,其中每個指針指向一顆子樹的根節點
孩子兄弟表示法:設置兩個指針:分別指向該節點的第一個孩子和此節點的右兄弟
二叉樹:n個結點的有限集合,該集合或者爲空集,或者由一個根節點和兩顆互不相交的、分別稱爲根節點的左子樹和右子樹的二叉樹組成
- 每個結點最多有兩顆子樹,二叉樹不存在度大於2的結點
- 左子樹和右子樹是有順序的,次序不能顛倒
斜樹:所有的結點都只有左子樹的二叉樹叫左斜樹;所有的結點只有右子樹的二叉樹叫右斜樹
滿二叉樹:在一棵二叉樹中,如果所有分支結點都存在左子樹和右子樹,並且所有葉子都在同一層上
完全二叉樹:對一棵具有n個結點的二叉樹按層序編號,如果編號爲i(1<=i<=n)的結點與同樣深度的滿二叉樹編號爲i的結點在二叉樹中位置完全相同
滿二叉樹一定是一棵完全二叉樹,但完全二叉樹不一定是滿的
二叉樹的性質
- 在二叉樹的第i層上至多有2的i-1次方個結點
- 深度爲k的二叉樹至多有2的k次方-1個結點
- 對任何一顆二叉樹T,如果其終端節點數爲N0,度爲2的節點數N2,N0 = N2+1
- 具有n個結點的完全二叉樹的深度爲[log2n]+1([x]表示不大於x的最大整數)
二叉樹的順序存儲結構
- 順序存儲結構一般只用於完全二叉樹
二叉樹的鏈式存儲結構
data、lchild和rchild
typedef struct BiTNode
{
TElemType data;
struct BiTNode* lchild,*rchild;
}BiTNode,*BiTree;
二叉樹的遍歷:從根節點出發,按照某種次序依次訪問二叉樹中所有節點,使得每個結點被訪問一次且僅被訪問一次
- 前序遍歷:若二叉樹爲空,則空操作返回,否則先訪問根節點,然後前序遍歷左子樹,再前序遍歷右子樹
- 中序遍歷:若二叉樹爲空,則空操作返回,否則從根節點開始(注意不是先訪問根節點),中序遍歷根節點的左子樹,然後是訪問根節點,最後中序遍歷右子樹;
- 後序遍歷:若二叉樹爲空,則空操作返回;從左到右先葉子後節點的方式遍歷左右子樹,最後是訪問根節點
前序遍歷算法:
void PreOrderTraverse(BiTree T)
{
if(T == NULL)
return;
printf("%c\n",T->data);
PreOrderTraverse(T->lchild);
PreOrderTraverse(T->rchild);
}
中序遍歷算法:
void InOrderTraverse(BiTree T)
{
if(T==UNLL)
return NULL;
InOrderTraverse(T->lchild);
printf("%c",T->data);
InOrderTraverse(T->rchild);
}
後序遍歷算法
void PostOrderTraverse(BiTree T)
{
if(T==NULL)
return;
PostOrderTraverse(T->lchild);
PostOrderTraverse(T->rchild);
printf("%c",T->data);
}