數據結構與算法回顧-1:算法的度量和基本數據結構

一、數據結構

數據結構是相互之間存在一種或多種關係的數據的集合。java

1.1 三要素

  1. 數據結構三要素是:1.數據的邏輯結構;2.數據的物理結構;3.數據的運算。
  2. 數據結構是相互之間存在一種或多種特定關係的數據元素的集合。

1.1.1 邏輯結構

分爲線性結構和非線性結構,node

  1. 線性結構:線性表、棧、隊列
  2. 非線性結構:樹、圖、集合

1.1.2 存儲結構

即物理結構,分四種(存儲數據時不僅要存儲元素的值,還要存儲數據元素間的關係)面試

  1. 順序存儲:存儲位置在物理上連續
  2. 連接存儲:鏈表形式(不一樣結點存儲空間不必定連續,可是結點內存儲單元地址必須連續)
  3. 索引存儲:使用 1 個數組記錄索引
  4. 散列存儲:根據元素關鍵字計算該元素存儲地址,如Hash存儲

1.1.3 邏輯結構

  1. 集合結構:數據元素除了同屬於一個集合以外,沒有其餘關係;
  2. 線性結構:數據元素之間是一對一的關係;
  3. 樹形結構:數據元素之間存在一種多對多的層次關係;
  4. 圖形結構:數據元素之間是多對多的關係;

1.2 數據類型

  1. 原子類型:不可再分的數據類型
  2. 結構類型:其值能夠再分紅若干成分的數據類型
  3. 抽象數據類型 (ADT):一個數學模型以及定義在該模型之上的一組操做,一般用數據對象、數據關係、基本運算操做集這樣三元組表示

二、算法的度量

2.1 時間複雜度

時間複雜度就是程序邏輯執行的次數。一般在求解時間複雜度的時候,會對其進行簡化。下面是推導大 O 階的方法:算法

  1. 用常數1取代運行時間中的全部加法常數
  2. 在修改後的運行次數函數中,只保留最高階項;
  3. 若是最高階項存在且不是 1,則去除與這個項相乘的常數。

常見的時間複雜度:數組

2.2.1 常數階

int num = 0, n = 100;
    num = (1 + n) * n / 2;
    printf(num);

主義上面的時間複雜度是常數階 O(1),而不是 O(3).數據結構

2.2.2 線性階

for(int i=0; i<n; i++) {
        // 執行時間複雜度爲O(1)的操做
    }

上面的時間複雜度是 O(n).架構

2.2.3 對數階

int cnt = 1;
    while (cnt < n) {
        cnt *= 2;
        // 執行時間複雜度爲O(1)的操做
    }

上面的時間複雜度是 O(logn).ide

2.2.4 平方階

int i, j;
    for (int i=0;i<n;i++) }
        for (int j=0;j<n;j++) {
           // 執行時間複雜度爲O(1)的操做
        }
    }

上面的計算的時間複雜度是 O(n2)函數

int i, j;
    for (int i=0;i<n;i++) }
        for (int j=0;j<m;j++) {
           // 執行時間複雜度爲O(1)的操做 
        }
    }

上面的計算的時間複雜度是 O(nm)學習

下面的程序的時間複雜度也是 O(n2):

int i, j;
    for (int i=0;i<n;i++) }
        for (int j=i;j<n;j++) {
           // 執行時間複雜度爲O(1)的操做
        }
    }

執行的次數,O(f(n)),其中 f(n) 是執行的次數,表示執行時間與 f(n) 成正比
時間複雜度的大小關係:

    O(1)<O(log2n)<O(n)<O(nLogn)<O(n^2)<O(n^3)<O(2^n)<O(n!)<O(nn)

2.2 空間複雜度

所需的物理存儲空間

    題目:算法
    void fun(int n){
        int i=1;
        while(i<=n) {
            i*=2;
        }
    }
    的時間複雜度是?
    思路:函數中運算次數最多的是 i*=2; 這一行,那麼假設它執行了 t 次,t 次時 i=2^t。
    所以,有:2^t<=n,因而得 t<=log2n ,因而可得 O(log2n)

三、線性表

線性表是具備相同數據類型的n個數據元素的有限序列。

3.1 順序表

線性表的順序存儲,即存儲在一組連續的存儲單元裏,如數組。順序表能夠是靜態分配的,也能夠是動態分配的。動態分配的,好比用指針指示動態數組,靜態分配的是建立數組的時候就指定數組的大小。

注意,在線性表當中插入或者刪除數據是要移動其餘元素的,而訪問的是否直接使用索引訪問便可。因此,對於線性表訪問第i個位置的元素的時間複雜度爲 O(1),在第i個位置插入元素的時間複雜度爲 O(n).

3.2 單鏈表

3.2.1 定義

普通的鏈表,定義的形式是

public class LinkedList<E> {
        transient int size = 0;
        transient Node<E> first;

        private static class Node<E> {
            E item;
            Node<E> next;

            Node(E element, Node<E> next) {
                this.item = element;
                this.next = next;
            }
        }
    }

這裏使用泛型的來表明鏈表的每一個節點中存儲的數據,使用內部類 Node 類定義鏈表的一個節點。

3.2.2 單鏈表的操做

  1. 每次創建鏈表時將結點插入到頭部:創建一個長度爲n的鏈表的時間複雜度是 O(n)
  2. 每次創建鏈表時將結點插入到尾部:創建一個長度爲n的鏈表的時間複雜度是 O(n)
  3. 插值節點:從頭結點出發,順着 next 指針往下找,時間複雜度爲 O(n)
  4. 按值查找表結點:時間複雜度爲 O(n)
  5. 插入和刪除結點操做:時間複雜度爲 O(1)
  6. 求結點長度:時間複雜度爲 O(n),遍歷,每次增長 1

3.3 雙向鏈表

3.3.1 定義

下面的是一份基於Java的雙向鏈表的實現:

public class LinkedList<E> {
        transient int size = 0;
        transient Node<E> first;
        transient Node<E> last;

        private static class Node<E> {
            E item;
            Node<E> next;
            Node<E> prev;

            Node(Node<E> prev, E element, Node<E> next) {
                this.item = element;
                this.next = next;
                this.prev = prev;
            }
        }
    }

3.4 靜態鏈表

藉助數組類描述鏈表,結點有數據域 data 和指針域 next,描述(這種設計的好處是適用於不支持指針的計算機語言)

3.5 棧

3.5.1 定義

棧是一種後進先出的數據結構,下面是一種使用單向鏈表實現的棧:

public class Stack<E>{
        private int size;
        private Node<E> first;

        private static class Node<E> {
            E element;
            Node<E> next;

            Node(E element, Node<E> next) {
                this.element = element;
                this.next = next;
            }
        }

        public void push(E element) {
            first = new Node<E>(element, first);
            size++;
        }

        public E pop() {
            if (size == 0) {
                throw new UnsupportedOperationException("The stack is empty.");
            }
            size --;
            Node<E> oldFirst = first;
            first = oldFirst.next;
            return oldFirst.element;
        }

        public boolean isEmpty() {
            return size == 0;
        }
    }

3.5.2 題目:

題1:3個不一樣的元素依次進棧,能獲得幾種不一樣的出棧序列?
5種,abc acb bac bca cba,最後一種若是以c開頭,那麼ab必然已經存入了棧中,取出的順序只能是ba,即以c開頭的只有cba. 

題2:a b c d e f 以所給的順序依次進棧,若在操做時容許出棧,這得不到的序列爲?
A fedbca    B bcafed    C dcefba    D cabdef
這種題應該從每一個選項的第一個字母入手,以C爲例,若是d處在第一個,那麼說明前面的a b c確定已經存在棧中,那麼它們必然按照c b a的順序出來;
若是題目中的c b a的出現次序不對,那麼就得不到。而後再使用相同的思路判斷第二個字符的狀況。答案D

3.6 隊列

3.6.1 普通隊列

隊列也是一種線性表,特性是先入先出,隊列和棧的主要區別是插入、刪除操做的限定不同。下面是基於 Java 的一種使用鏈表來實現的隊列:

public class Queue<E> {
        private Node<E> first, last;
        private int size;

        private static class Node<E> {
            E element;
            Node<E> next;

            Node(E element, Node<E> next) {
                this.element = element;
                this.next = next;
            }
        }

        public void enqueue(E element) {
            size++;
            Node<E> node = new Node<E>(element, null);
            if (last == null) {
                first = node;
                last = node;
                return;
            }
            last.next = node;
            last = node;
        }

        public E dequeue() {
            if (size == 0) {
                throw new UnsupportedOperationException("The queue is empty.");
            }
            size--;
            E element = first.element;
            first = first.next;
            return element;
        }

        public boolean isEmpty() {
            return size == 0;
        }
    }

3.6.2 雙端隊列

隊首和隊尾都容許入隊和出隊的隊列。

3.6.3 應用:

  1. 棧在括號匹配中的應用:與 [([][])] 相似的括號匹配的問題,遇到一個左括號,判斷是否合法,若合法就先存在棧中,等待右括號出現,看是否匹配;
  2. 棧在表達式求值中的應用:中綴:A+B(C-D)-E/F,後綴:ABCD-+EF/-,而後按照計算的規則,依次進棧、出棧便可求得表達式的結果;
  3. 棧在遞歸中的應用:遞歸函數在求解的時候,要不斷返回結果、傳入參數等,所以效率不高,能夠藉助棧將遞歸問題轉換爲非遞歸問題;
  4. 隊列在層次遍歷中的應用:好比遍歷二叉樹等;
  5. 隊列在計算機系統中的應用:用於任務分配,當任務沒法馬上所有完成的時候,對沒法馬上完成的任務,先將其存入到隊列中,等待其餘任務完成以後再去執行這些任務。

3.7 揹包

揹包是一種不支持從中刪除元素的集合數據類型,它的目的就是幫助用例收集並迭代遍歷全部收集到的元素。使用揹包的不少場景可能使用棧或者隊列也能實現,可是使用揹包能夠說明元素存儲的順序不重要。下面的是一份基於 Java 的揹包的實現,在這裏咱們只是在以前棧的代碼的基礎之上作了一些修改,並讓其實現 Iterable 接口以在 foreach 循環中使用揹包遍歷元素:

public class Bag<E> implements Iterable<E>{
        private int size;
        private Node<E> first;

        private static class Node<E> {
            E element;
            Node<E> next;

            Node(E element, Node<E> next) {
                this.element = element;
                this.next = next;
            }
        }

        public void add(E element) {
            first = new Node<E>(element, first);
            size++;
        }

        public Iterator<E> iterator() {
            return new ListIterator();
        }

        private class ListIterator implements Iterator<E> {
            private Node<E> current = first;

            @Override
            public boolean hasNext() {
                return current != null;
            }

            @Override
            public E next() {
                E element = current.element;
                current = current.next;
                return element;
            }

            @Override
            public void remove() {}
        }

        public boolean isEmpty() {
            return size == 0;
        }
    }

四、非線性表

4.1 樹

4.1.1 概念

  1. 根:樹的最上層的頂點;
  2. 父結點:某個節點上面的節點;
  3. 祖先結點:父節點的父節點等;
  4. 結點的度:樹中結點的子結點個數;
  5. 樹的度:樹中結點的最大度數;
  6. 分支結點:度大於 0 的結點;
  7. 葉子結點:度等於 0 的結點;
  8. 樹的高度:樹中結點的最大層數
  9. 路徑:樹中兩個結點之間所通過的結點序列
  10. 路徑長度:路徑上通過的邊的個數
  11. 樹的路徑長度:從樹根到每一結點的路徑長度之和
  12. 森林:多個樹就組成了森林,只要把樹個根結點刪去就成了森林,只要給n課樹加上結點,就成了樹。

4.1.2 題目

題:一棵有n個結點的樹,全部結點的度數之和爲_______.
問題轉換成:一棵有3個結點的樹,全部結點的度數之和爲____. 由於題目是選擇,因此應該儘可能簡化題目。答案n-1

4.2 二叉樹

  1. 二叉樹:每一個結點的度不大於2的樹;
  2. 滿二叉樹:除了葉子結點,其餘結點的度都爲2,也就是不存在只有1個結點的分支;
  3. 徹底二叉樹:相對於滿二叉樹,它有的結點度不爲2,可是存在的分支的編號與滿二叉樹相同

image

在上圖中左側的是徹底二叉樹,右側的是滿二叉樹。

說明:所謂的編號就是指每層從左到右,按照滿二叉樹的形式編號,上面的滿二叉樹每一個結點的值就是它們的編號。這個編號也是咱們在使用順序存儲的時候對應數組的下標。

4.2.1 二叉樹的存儲結構

1.順序存儲

這種存儲方式,能夠理解爲使用數組存儲,注意開始存儲的下標是1,這是爲了與二叉樹的性質對應,另外有時候咱們也能夠將數組的第一個元素做爲哨兵。順序存儲的基本思想是,按照滿二叉樹的編號順序,若是指定編號(其實就是數組的下標)處有結點的話,數組指定位置的值即爲結點的值,不然爲0(表示空結點)。固然,也能夠在各個元素中存放一些具備具體含義的值。

如圖所示的樹在數組中的實際存儲爲:- 1 2 3 0 4 0 5 0 0 6 0(第0位不使用)。

2.鏈式存儲

下面是使用Java代碼實現的一個二叉樹,這裏每一個結點要包含左右兩個子結點以及相應的數據實體。顯然,

public class Tree<E> {
        private Node<E> root;

        private static class Node<E> {
            E element;
            Node<E> leftChild;
            Node<E> rightChild;

            Node(Node<E> leftChild, E element, Node<E> rightChild) {
                this.leftChild = leftChild;
                this.element = element;
                this.rightChild = rightChild;
            }
        }
    }

4.2.2 遍歷

  1. 先序遍歷:a.訪問根結點  b.按照先序遍歷左子樹  c.按照先序遍歷右子樹
  2. 中序遍歷:a.按照中序遍歷左子樹  b.訪問根結點  c.按照中序遍歷右子樹
  3. 後序遍歷:a.按照後序遍歷右子樹  b.按照後序遍歷左子樹  c.訪問根結點
  4. 層次遍歷:藉助隊列,先將根節點入隊,而後出隊,訪問該結點,將其子結點入隊,訪問其根結點...直到隊列爲空。

說明:上述三幅圖從左到右的遍歷方式依次是先序遍歷、中序遍歷和後序遍歷。

做爲練習,這裏給出遍歷二叉樹的方法,能夠觀察一下二叉樹的實現,以及中序遍歷的時候輸出的二叉樹的值,其中 outNode() 方法用來輸出結點的值:

/* 先序遍歷 */
    public void former() {
        former(root);
    }

    private void former(Node<Key, Value> node) {
        if (node == null) return;
        outNode(node); // 先序
        former(node.leftChild);
        former(node.rightChild);
    }

    /* 中序遍歷,注意中序輸出的結果和排序的結果的關係 */
    public void center() {
        center(root);
    }

    private void center(Node<Key, Value> node) {
        if (node == null) return;
        center(node.leftChild);
        outNode(node); // 中序
        center(node.rightChild);
    }

    /* 後序遍歷 */
    public void latter() {
        latter(root);
    }

    private void latter(Node<Key, Value> node) {
        if (node == null) return;
        latter(node.leftChild);
        latter(node.rightChild);
        outNode(node); // 後序
    }

結論:

  1. 能夠看出所謂的中序、先序和後序的主要區別就在於輸出遍歷結果時候的位置;
  2. 中序遍歷的時候輸出結果就是二叉堆的元素的從小到大的排序結果。
  3. 那麼從上面的操做中能夠看出,所謂的中序就是將輸出操做夾在了兩個遞歸之間。所以,若是指定的樹不是二叉樹,而是多叉樹,也就是咱們使用一個列表來保存各個子結點,那麼是沒有中序輸出的。

4.2.3 構造二叉樹

注意若是隻知道二叉樹的先序序列和後序序列是沒法惟一地肯定一棵二叉樹的。

image

如圖所示的二叉樹,它的三種遍歷方式:

  1. 先序遍歷結果是:-  +  a  *  b  -  c  d  /  e  f
  2. 中序遍歷結果是:a  +  b  *  c  -  d  -  e  /  f
  3. 後序遍歷結果是:a  b  c  d  -  *  +  e  f  /  -

說明:先序遍歷的時候,根結點處在第1的位置;中序遍歷的時候根節點前面是左子樹,後面是右子樹;後序遍歷的時候,根結點處在最後的位置。能夠根據上面的思路,依次進行判斷,從而能夠寫出完整的樹。

4.2.4 二叉樹的性質

  1. 二叉樹的第i層上至多有 2i-1 個結點(i>=1)
  2. 深度爲k的二叉樹至多有 2k-1 個結點(k>=1)
  3. 對任何一棵二叉樹T,若是其終端結點數爲 n0,度爲 2 的結點樹爲 n2,則 n0=n2+1.
  4. 具備 n 個結點的徹底二叉樹的深度爲 (log2n + 1) 向下取整的結果
  5. 對於一顆有 N 個結點的徹底二叉樹的結點按層序編號,對任一結點 i,有
  6. 其父結點是 i/2 向下取整的結果
  7. 左孩子是 2i,右孩子是 2i+1

五、圖

5.1 術語

  1. 若是兩個頂點經過一條邊相連,咱們就稱這兩個頂點是相鄰的,並稱這條邊依附於這兩個頂點。某個頂點的度數,即爲依附於它的邊的總數;
  2. 若是從任意一個頂點都存在一條路徑達到另外一個頂點,就成這幅圖是連通圖;
  3. 樹是一幅無環圖,互不相連的樹組成的集合稱爲森林。
  4. 有向圖:邊是帶方向的,假如邊是從 A 到 B 的,那麼從 A 到 B 能夠,從 B 到 A 就不行,直觀來說就是圖的邊帶箭頭;
  5. 無向圖:邊不帶方向,若是能從 A 到 B 就能從 B 到 A,直觀來說就是圖的邊不帶箭頭;
  6. 徹底圖:無向圖中任意兩個頂點之間都存在邊的爲無向徹底圖;有向圖中,任意兩個頂點之間都存在方向相反的弧的是有向徹底圖。
  7. 頂點的度、入度和出度:頂點的度是依附於該點的邊的數目,對於有向圖又有入度和出度之分,頂點的度等於入度+出度。
  8. 一條有向邊的第一個頂點稱爲它的頭,第二個頂點稱爲它的尾。
  9. 在一幅有向圖中,兩個頂點之間的關係有四種:不相連;存在從v到w的邊;存在從 w 到 v 的邊;同時存在從v到w的邊和從 w 到 v 的邊。
  10. 有向環 是一條至少含有一條邊且起點和終點相同的有向路徑。
  11. 簡單有向環 是一條除了起點和終點必須相同以外,不含重複的頂點和邊的環。
  12. 有向五環圖(DAG) 是一幅不含有向環的有向圖。
  13. 強連通:若是兩個頂點 v 和 w 是相互可達的,則稱它們是強連通的,若是一幅圖任意兩個頂點都是強連通的,則稱這幅圖是強連通的。
  14. 強連通份量:一幅圖中的強連通的頂點構成的最大子集,一幅圖能夠有多個強連通份量組成。

5.2 圖的存儲

5.2.1 鄰接矩陣

使用一個V*V矩陣來表示,其中V是頂點的個數。矩陣的某個元素中存儲的能夠是布爾變量,表示該邊是否存在,在實際應用中也能夠將其設置爲某個邊的權重。使用這種方式的缺點是當數據量比較大的時候會佔用很大的存儲空間。

5.2.2 鄰接表數組

即便用一個以頂點爲數組的索引的列表數組來表示圖。如下是圖的一個基於Java的實現:

public class Graph {
        private Bag<Integer>[] adj;

        public Graph(int V) {
            adj = new Bag[V];
            for (int i=0;i<V;i++) {
                adj[i] = new Bag<Integer>();
            }
        }
    }

能夠看出實際在這裏咱們使用了揹包來存儲圖的數據。adj 的具體含義是:好比,若是頂點 0 對應的數組元素是 adj[0],這裏的 adj[0] 是一個揹包,它裏面存儲了與頂點 0 相連的其餘頂點。若是是無向圖的話,只要在揹包中存儲全部相關的頂點就能夠了。若是是有向圖的話,須要存儲該點指向的頂點的值。

5.2.3 鄰接多重表

它跟鄰接表的區別在於,在鄰接表中,同一條邊由用兩個頂點表示,而在鄰接多重表中只用一個頂點表示。

5.2.4 邊集數組

上圖就是邊集數組的表示方法,能夠看出它使用一個數組來存儲各個邊。

數據結構資料+面試架構資料 免費分享 點擊連接 便可領取

《Android架構師必備學習資源免費領取(架構視頻+面試專題文檔+學習筆記)》

相關文章
相關標籤/搜索