學習筆記:大話數據結構(一)

第一章 數據結構緒論


邏輯結構與物理結構:node

  • 邏輯結構指數據對象中數據元素之間的相互關係;分爲集合結構、線性結構、樹形結構、圖形結構四種;
  • 物理結構指數據的邏輯結構在計算機中的存儲形式;分爲順序存儲、鏈式存儲兩種;

第二章 算法


定義:解決特定問題求解步驟的描述,在計算機中變現爲指令的有限序列,而且每條指令表示一個或多個操做。
特性:輸入輸出、有窮性、肯定性、可行性
算法設計要求:正確性、可讀性、健壯性、時間效率高和存儲量低
效率度量方法:過後統計、事前分析估算算法

時間複雜度:算法語句總執行次數T(n)是關於問題規模n的函數,記做T(n)=O(f(n)),即大O階。數組

推導大O階:數據結構

  1. 用常數1取代運行時間中的全部加法常數;
  2. 在修改後的運行次數函數中,只保留最高階項;
  3. 若是最高階項存在且不爲1,則除去與這個項相乘的常數,獲得的結果就是大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時稱爲空表;
線性表數據元素:一個線性表的全部數據元素要相同類型的數據;複雜表中,一個數據元素能夠有多個數據項組成;函數

線性表順序存儲結構:

指用一段地址連續的存儲單元依次存儲線性表的數據元素,可用一位數組實現。其實順序存儲結構就是一個數組的初始化,即聲明一個類型和大小的數組並賦值的過程;須要三個屬性:性能

  • 存儲空間的起始位置:數組data的存儲位置就是存儲空間的存儲位置;
  • 線性表的最大存儲容量:數組長度MaxSize;
  • 線性表當前長度:length;

存儲地址計算方法:存儲器中的每一個存儲單元都有本身的編號,這個編號稱爲地址;設計

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;
}

單鏈表存儲結構與順序存儲結構的對比:

  • 存儲分配方式:
    • 順序存儲結構用一段連續的存儲單元依次存儲線性表的數據元素;
    • 單鏈表採用鏈式存儲結構,用一組任意的存儲單元存放線性表的元素;
  • 時間性能:
    • 查找:
      • 順序結構O(1)
      • 單鏈表O(n)
    • 插入和刪除:
      • 順序存儲結構須要平均移動表長一半的元素,時間爲O(n)
      • 單鏈表在給出某位置的指針後,插入、刪除的時間僅爲O(1)
  • 空間性能:
    • 順序存儲結構須要預分配存儲空間,分多了形成內存浪費,分少了已發生內存上溢
    • 單鏈表不須要分配存儲空間,只要有剩餘空間就能夠分配,結點個數不受限制

得出結論:

  • 若是線性表須要頻繁查找,不多進行插入和刪除操做的時候,宜採用順序存儲結構;若須要頻繁插入和刪除的時候,宜採用單鏈表結構;
  • 當線性表中的元素個數變化較大或者根本不知道有多大時,最好用暗單鏈表結構,這樣能夠不用考慮存儲空間的大小問題;而若是事先知道線性表的大體長度,好比一星期就是7天,一年就是12個月,這些狀況用順序存儲結構效率就會高出不少。

靜態鏈表:用數組描述的鏈表

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;
}

靜態鏈表的優缺點:

  • 優勢:在插入和刪除操做時,只須要修改遊標,不須要移動元素,從而改進了在順序存儲結構中插入刪除元素須要移動大量數據的缺點;
  • 缺點:1. 沒有解決連續存儲分配致使表長難以肯定的問題;2. 失去了順序存儲結構隨機存儲的特性;

單循環鏈表

將單鏈表的終端結點的指針端由空指針改成指向頭結點,就是單鏈表造成一個首尾相接的環形,稱爲單循環鏈表,簡稱循環鏈表。

指向終端結點的指針是尾指針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的頭結點內存*/
相關文章
相關標籤/搜索