邏輯結構與物理結構:node
定義:解決特定問題求解步驟的描述,在計算機中變現爲指令的有限序列,而且每條指令表示一個或多個操做。
特性:輸入輸出、有窮性、肯定性、可行性
算法設計要求:正確性、可讀性、健壯性、時間效率高和存儲量低
效率度量方法:過後統計、事前分析估算算法
時間複雜度:算法語句總執行次數T(n)是關於問題規模n的函數,記做T(n)=O(f(n)),即大O階。數組
推導大O階:數據結構
常見時間複雜度:
常數階O(1)<對數階O(logN)<線性階O(N)<O(N*logN)<平方階O(N^2)<立方階O(N^3)<乘方階O(2^N)<階乘階O(N!)<O(N^N)app
平均運行時間:指望的運行時間;
最壞運行時間:是一種運行時間的保證。一般,除非特別指定,提到的運行時間都是最壞運行時間;dom
線性表(List):零個或多個數據元素的有限序列;元素之間有序,如有多個元素,則第一個元素無前驅,最後一個元素無後繼,其他每一個元素都有且只有一個前驅和後繼;
線性表長度:線性表元素的個數n(n>=0),當n=0時稱爲空表;
線性表數據元素:一個線性表的全部數據元素要相同類型的數據;複雜表中,一個數據元素能夠有多個數據項組成;函數
指用一段地址連續的存儲單元依次存儲線性表的數據元素,可用一位數組實現。其實順序存儲結構就是一個數組的初始化,即聲明一個類型和大小的數組並賦值的過程;須要三個屬性:性能
存儲地址計算方法:存儲器中的每一個存儲單元都有本身的編號,這個編號稱爲地址;設計
private int OK = 1; private int ERROR = 0; private int TRUE = 1; private int FALSE = 0; // 獲取線性表第i個位置的元素,賦給e DataType getEleFromList(List L, int i, DataType d) { if (L.length == 0 || i<1 || i > L.length) { // 判斷空表或下標越界 return ERROR; } d = L.data[i-1]; return OK; } /** * 1. 插入位置不合理則返回異常; * 2. 線性表長度大於數組長度,則返回異常或者增長容量; * 3. (在不須要輔助內存,在原表操做的前提下)從最後一個元素開始向前遍歷到第i個位置,分別都後移一位; * 4. 將要插入的元素填入位置i處; * 5. 線性表長度+1; * / void insertEleToList(List L, int i, DataType d) { int k; if (L.length == MAXSIZE) { // 線性表已滿 return ERROR; } if (i<1 || i > L.length+1) { // i不在範圍內 return ERROR; } if (i<L.length) { // 插入位置不在表尾 for (k = L.length - 1; k >= i-1; k--) { L.data[k+1] = L.data[k]; } } L.data[i-1] = d; // 插入新元素d L.length++; // 表長+1 return OK; } /** * 若是刪除位置不合適,則返回異常; * 取出要刪除的元素; * 從刪除元素位置開始遍歷到最後一個元素位置,分別把他們都向前移動一個位置; * 表長-1; * / DataType deleteEleFromList(List L, int i, DataType d) { int k; if (L.length == 0) { // 線性表爲空 return ERROR; } if (i<1 || i>L.length) { // 刪除位置不正確 return ERROR; } d = L.data[i-1]; if (i<L.length) { // 若是刪除的不是最後位置 for (k = i; k < L.length; k++) { L.data[k-1] = L.data[k]; // 將刪除位置的後繼元素前移 } } L.length--; // 表長-1 return OK; }
總結:線性表的順序存儲結構,在存或讀數據時時間複雜度都是O(1),而插入或刪除數據時時間複雜度都是O(n);
優勢:指針
缺點:
線性表的鏈式存儲結構特色:用一組任意的存儲單元來存儲線性表的結點,這組存儲單元能夠是連續的,也能夠是不連續的;這就意味着這些元素能夠在內存未被佔用的任意位置。
與線性表順序結構的區別:順序結構中每一個數據元素只須要存儲數據元素信息便可,鏈式結構中,要存儲數據元素信息和它的後繼元素的內存地址;
數據域:存儲數據元素信息的域;指針域或鏈:存儲直接後繼位置的域;數據域和指針域組成鏈表的存儲映像,稱爲結點(Node);由多個結點組成鏈表,由於每一個結點只包含一個指針域,又稱爲單鏈表;
第一個結點的存儲位置稱爲頭指針;最後一個結點指針爲空(用Null或^表示);有時爲了方便對鏈表進行操做,會在單鏈表的第一個結點前附設一個結點稱爲頭結點,頭結點的數據域能夠不存儲任何信息,也能夠通常存儲線性表的長度等附加信息,頭結點的指針域存儲指向第一個結點的指針;
頭指針:
頭結點:
LinkedListNode node = new LinkedListNode();
node.data 表示該結點數據域存儲的數據信息;node.next表示該結點指針域存儲的地址值;
/** * 獲取單鏈表第i個數據:getEleFromLinkedList() * 1. 聲明一個指針current指向鏈表的第一個結點,初始化計數器j從1開始; * 2. 當j<i時就遍歷鏈表,讓指針current向後移動,不斷指向下一結點,j累加1; * 3. 若到鏈表末尾current爲空,則說明第i個結點不存在; * 4. 不然查找成功,返回指針current指向結點的數據; */ DataType getEleFromLinkedList(LinkedListNode head, int i, DataType d) { int j = 1; // 計數器 LinkedLinstNode current = head; // 當前指針current指向表頭head while (current != null && j<i) { // 指針current不爲空,且計數器j<i時,繼續循環 current = current.next; ++j; } if (current == null || j>i) { // 指針current爲空,或計數器j>i時,則第i個結點不存在,返回異常 return ERROR; } d = current.data; // 接收第i個結點數據值 return OK; // 返回查找成功 } /** * 單鏈表第i個位置插入結點insertNodeToLinkedList(LinkedListNode head, int i, DataType d) * 1. 聲明一個指針current指向鏈表的第一個結點,初始化計數器j從1開始; * 2. 當j<i時就遍歷鏈表,讓指針current向後移動,不斷指向下一結點,j累加1; * 3. 若到鏈表末尾current爲空,則說明第i個結點不存在; * 4. 不然查找成功,建立一個空節點node並將要插入額數據d賦值給node,即node.data = d; * 5. 執行單鏈表插入語句:node.next = current.next; current.next = node; */ DataType insertNodeToLinkedList(LinkedListNode head, int i, DataType d) { int j = 1; LinkedListNode current = head; while (current != null && j<i) { // 單鏈表不爲空且還未遍歷帶第i個結點,繼續循環 current = current.next; ++j; } if (current == null || j>i) { // 單鏈表爲空或第i個結點不存在,返回異常 return ERROR; } LinkedListNode node = new LinkedListNode(d); // 傳入參數d,建立要插入的結點 node.next = current.next; // 把當前結點的指針域賦給新建的空結點node current.next = node; // 把當前結點額指針域指向新建的空結點node,千萬注意這兩行順序不能顛倒 return OK; } /** * 刪除單鏈表第i個數據:deleteNodeFromLinkedList() * 1. 聲明一個指針current指向鏈表的第一個結點,初始化計數器j從1開始; * 2. 當j<i時就遍歷鏈表,讓指針current向後移動,不斷指向下一結點,j累加1; * 3. 若到鏈表末尾current爲空,則說明第i個結點不存在; * 4. 不然查找成功,把第i個結點後繼元的地址值賦值給第i個結點前驅元的指針域; * 5. 把第i個結點存儲的數據賦值給變量d並返回,此時系統會回收第i個結點的內存; */ DataType deleteNodeFromLinkedList(LinkedListNode head, int i, DataType d) { int j = 1; LinkedListNode current = head; while (current.next != null && j<i-1) { // 單鏈表不爲空且j<i-1,繼續循環 current = current.next; ++j; } if (current.next == null && j>i) { // 單鏈表爲空或者不存在第i個結點,返回異常 return ERROR; } current.next = current.next.next; // 把第(i-1)個結點的後繼元的後繼元地址值賦值給第(i-1)個結點的指針域 d = current.next.data; // 接收第i個結點數據並返回 return d; }
單鏈表插入、刪除結點的總結:對於插入或刪除結點越頻繁的操做,單鏈表的效率就越高;
/** * 單鏈表整表建立:頭插法createLinkedListFromHead(int n) * 1. 聲明一個指針current和計數變量i; * 2. 初始化一個空鏈表list,list頭結點指向null,即建立一個帶頭結點的空表; * 3. 循環:生成一新結點賦值給current;隨機生成一數字賦值給current的數據域current.data; * 4. 將current插入到頭結點與前一新節點之間; */ void createLinkedListFromHead(int n) { LinkedListNode head = new LinkedListNode(); // 建立頭結點 head.next = null; node.next = null; for (int i = 0; i<n; i++) { LinkedListNode current = new LinkedListNode(); // 建立要插入的結點 int num = Math.random/100 + 1;// 生成100之內的隨機數num current.data = num; // 賦值給current.data current.next = head.next; // 把頭結點的指針域賦值給新結點 head.next = current; // 新節點始終插入到頭結點的後面,即第一個結點的位置 } } /** * 單鏈表整表建立:尾插法createLinkedListFromTail(int n) */ void createLinkedListFromTail(int n) { int num = Math.random/100 + 1;// 生成100之內的隨機數num LinkedListNode tail = new LinkedListNode(); // 建立尾結點 for (int i = 0; i<n; i++) { LinkedListNode current = new LinkedListNode(); // 建立要插入的結點 int num = Math.random/100 + 1;// 生成100之內的隨機數num current.data = num; // 賦值給current.data tail.next = current; tail = current; // 讓新結點變成新的尾結點 } tail.next = null; // 尾結點指針域置空 }
當咱們不須要使用這個單鏈表時,須要把它銷燬掉,也就是在內存中將他釋放掉。
/** * 單鏈表整表刪除 * 1. 建立兩個結點current和runner * 2. 把第一個結點賦給current * 3. 循環:將下一個結點賦值給runner;釋放current;將runner賦給current; */ void clearAllLinkedList(LinkedList list) { LinkedListNode current = new LinkedListNode(); LinkedListNode runner = new LinkedListNode(); current = list.next; while (current != null) { runner = current.next; /*釋放current結點 free(current)*/ current = runner; } list.next = null; }
得出結論:
C語言經過指針能夠很是容易的操做內存中的地址和數據;Java等面嚮對象語言啓用對象引用機制間接實現了指針的某些做用。
讓數組的每一個元素都有兩個數據域組成,data和cur,也就是數組每一個元素的下標都對應一個data和cur。數據域data用來存放數據元素,也就是咱們要處理的數據;數據域cur至關於單鏈表的指針,用來存放該元素的後繼在數組中的下標,咱們把cur叫作遊標!咱們一般會把數組建立的大些,以免插入較多數據時不至於形成溢出。
另外對數組的第一個元素和最後一個元素作特殊處理,不存數據。把未被使用的數組元素稱爲備用鏈表。而數組第一個元素,即下標爲0的元素的cur存放備用鏈表的第一個數組元素的下標;而最後一個元素的cur存放第一個有存儲數據的數組元素的下標,至關於單鏈表中頭結點的做用;有存儲數據的最後一個數組元素的cur存放0表示下一個數組元素爲空;
/** * 建立(初始化)靜態鏈表 */ void initStaticLinkedList(StaticLinkedList list, int listSize) { for (int i = 0; i<listSize-1; i++) { list[i].cur = i+1; } list[listSize-1] = 0; // 最後那個數組元素的cur爲0,表示數組的第一個元素 return SUCCESS; // 目前靜態鏈表爲空 }
靜態鏈表要解決的問題:如何用靜態鏈表模擬動態鏈表結構的存儲空間的分配:須要時申請,無用時釋放?
解決辦法是:把全部未被使用的及已被刪除的結點用遊標鏈成一個備用的鏈表,每當須要插入數據時,就能夠從備用鏈表上獲取第一個結點做爲待插入的結點。
/*若備用鏈表非空則返回分配的結點下標,不然返回0*/ int applyMemory(StaticLinkedList list) { int i = list[0].cur; // 獲取當前靜態鏈表小標爲0的元素的cur值,也就是要返回的第一個備用的空閒下標 if (list[0].cur != null) { list[0].cur = list[i].cur; // 因爲第一個備用鏈表的第一個結點被拿來用了,就把它的cur值賦給lis //[0].cur來記錄 } return i; } /*靜態鏈表插入結點實現,在位置i插入數據d*/ void insertToStaticLinkedList(StaticLinkedList list; int i; DataType d) { int j=list[0].cur, k=list.length-1; // j爲第一個空閒元素的下標,k爲最後一個元素的下標 if (i<1 || i>list.length) { return ERROR; } if (j != null) { // 表示有空閒元素,繼續執行 list[j].data = d; // 把要插入的數據賦給第一個空閒元素的data,即list[j].data for (l=1; l<=i-1; l++) { // 找到位置i的前一個元素 k = list[k].cur; } list[j].cur = list[k].cur; // 把位置(i-1)元素的cur值賦給新插入結點的cur,list[j].cur list[k].cur = j; // 把新插入結點的下標賦給位置(i-1)元素cur return OK; } return ERROR; } /*靜態鏈表的刪除操做,刪除靜態鏈表位置i的結點*/ DataType deleteFromStaticLinkedList(StaticLinkedList list, int i) { DataType d = null; int j, k=list.length-1; if (i<1 || i>list.length) { return ERROR; } for (j=1; j<=i-1; j++) { k = list[k].cur; // k就是位置i結點的下標值,此時j=i-1 } j = list[k].cur; // 把位置i結點存儲的cur值(也即位置爲(i+1)結點的下標值)賦給變量j list[k].cur = list[j].cur; // 位置爲i結點存儲的cur值,賦給位置(i-1)結點的cur值 /*free(list[j]),即將結點j回收到備用鏈表*/ d = list[k].data; // 接收結點i的data值並返回 list[k].cur = list[0].cur; // 此時k=i,要將結點i回收,就要把結點i插入到第一個備用結點的位置,把list //[0].cur賦給list[k].cur list[0].cur = k; // 而後讓list[0].cur存儲k值,即結點i的下標 return d; } /*獲取靜態鏈表的長度*/ int getStaticLinkedListLength(StaticLinkedList list) { int length = 0; int i = list[SIZE-1].cur; // 拿到第一個存儲數據的結點下標值 while (i != null) { i = list[1].cur; length++; } return legnth; }
靜態鏈表的優缺點:
將單鏈表的終端結點的指針端由空指針改成指向頭結點,就是單鏈表造成一個首尾相接的環形,稱爲單循環鏈表,簡稱循環鏈表。
指向終端結點的指針是尾指針rear,則能夠很方便的訪問終端結點(rear)和頭結點(rear.next)了,訪問終端結點的時間是O(1),訪問頭結點的時間是O(2);此時能夠很方便的把兩個循環鏈表連接成一個循環鏈表:
int listAHead = rearA.next; // listAHead用來保存A鏈表的頭結點 rearA.next = rearB.next.next; // 把B鏈表的第一個結點(即rearB.next.next)賦值給A鏈表的尾結點指針 int listBHead = rearB.next; // listBHead用來保存鏈表B的頭結點 rearB.next = listAHead; // 讓鏈表B的尾結點指針指向A鏈表的頭結點 /*free(listBHead)釋放鏈表B的頭結點內存*/
雙向鏈表是在單鏈表的每一個結點再加一個指針域指向其前驅結點,因此雙向鏈表的每一個結點都有兩個指針域,一個指針域指向其後繼結點,另外一個指針域指向其前驅結點。
雙向循環空鏈表的只有一個頭結點,其頭指針指向頭結點,前驅指針指向頭結點,後繼指針也指向頭結點;
循環非空帶頭結點雙鏈表的看似複雜,可是其基本操做和單鏈表是同樣的,咱們只要使用一個方向的指針便可;另外一方面,因爲其具備兩個方向的指針,那麼就能夠反向遍歷鏈表,這很方便查找操做,可是增刪操做卻更麻煩了,刪除操做以前須要更改兩個指針變量,插入操做時順序千萬不能寫反!
/*在結點p和結點p.next之間插入結點s*/ s.prev = p; // 1. 搞定s結點的前驅 s.next = p.next; // 2. 搞定s結點後繼 p.next.prev = s; // 3. 搞定後結點的前驅 p.next = s; // 4. 搞定前結點的後繼 /*千萬注意二、三、4步的順序不能寫反,由於二、3步須要用到p.next,若是先執行第4步爲p.next賦值的操做,就會致使不能插入新結點*/ /*刪除結點p*/ p.prev.next = p.next; // 把p的後繼指針賦值給p前驅元的後繼指針 p.next.prev = p.prev; // 把p的前驅指針賦值給p後繼元的前驅指針 /*free(p)釋放鏈表B的頭結點內存*/