1. 程式人生 > >常見資料結構與演算法整理總結

常見資料結構與演算法整理總結

資料結構是以某種形式將資料組織在一起的集合,它不僅儲存資料,還支援訪問和處理資料的操作。演算法是為求解一個問題需要遵循的、被清楚指定的簡單指令的集合。下面是自己整理的常用資料結構與演算法相關內容,如有錯誤,歡迎指出。

為了便於描述,文中涉及到的程式碼部分都是用Java語言編寫的,其實Java本身對常見的幾種資料結構,線性表、棧、佇列等都提供了較好的實現,就是我們經常用到的Java集合框架,有需要的可以閱讀這篇文章。Java - 集合框架完全解析

一、線性表
  1.陣列實現
  2.連結串列
二、棧與佇列
三、樹與二叉樹
  1.樹
  2.二叉樹基本概念
  3.二叉查詢樹
  4.平衡二叉樹
  5.紅黑樹
四、圖
五、總結

一、線性表

線性表是最常用且最簡單的一種資料結構,它是n個數據元素的有限序列。

實現線性表的方式一般有兩種,一種是使用陣列儲存線性表的元素,即用一組連續的儲存單元依次儲存線性表的資料元素。另一種是使用連結串列儲存線性表的元素,即用一組任意的儲存單元儲存線性表的資料元素(儲存單元可以是連續的,也可以是不連續的)。

陣列實現

陣列是一種大小固定的資料結構,對線性表的所有操作都可以通過陣列來實現。雖然陣列一旦建立之後,它的大小就無法改變了,但是當陣列不能再儲存線性表中的新元素時,我們可以建立一個新的大的陣列來替換當前陣列。這樣就可以使用陣列實現動態的資料結構。

  • 程式碼1 建立一個更大的陣列來替換當前陣列
int
[] oldArray = new int[10]; int[] newArray = new int[20]; for (int i = 0; i < oldArray.length; i++) { newArray[i] = oldArray[i]; } // 也可以使用System.arraycopy方法來實現陣列間的複製 // System.arraycopy(oldArray, 0, newArray, 0, oldArray.length); oldArray = newArray;
  • 程式碼2 在陣列位置index上新增元素e
//oldArray 表示當前儲存元素的陣列
//size 表示當前元素個數
public void add(int index, int e) {

    if (index > size || index < 0) {
        System.out.println("位置不合法...");
    }

    //如果陣列已經滿了 就擴容
    if (size >= oldArray.length) {
        // 擴容函式可參考程式碼1
    }

    for (int i = size - 1; i >= index; i--) {
        oldArray[i + 1] = oldArray[i];
    }

    //將陣列elementData從位置index的所有元素往後移一位
    // System.arraycopy(oldArray, index, oldArray, index + 1,size - index);

    oldArray[index] = e;

    size++;
}

上面簡單寫出了陣列實現線性表的兩個典型函式,具體我們可以參考Java裡面的ArrayList集合類的原始碼。陣列實現的線性表優點在於可以通過下標來訪問或者修改元素,比較高效,主要缺點在於插入和刪除的花費開銷較大,比如當在第一個位置前插入一個元素,那麼首先要把所有的元素往後移動一個位置。為了提高在任意位置新增或者刪除元素的效率,可以採用鏈式結構來實現線性表。

連結串列

連結串列是一種物理儲存單元上非連續、非順序的儲存結構,資料元素的邏輯順序是通過連結串列中的指標連結次序實現的。連結串列由一系列節點組成,這些節點不必在記憶體中相連。每個節點由資料部分Data和鏈部分Next,Next指向下一個節點,這樣當新增或者刪除時,只需要改變相關節點的Next的指向,效率很高。

單鏈表的結構

下面主要用程式碼來展示連結串列的一些基本操作,需要注意的是,這裡主要是以單鏈表為例,暫時不考慮雙鏈表和迴圈連結串列。

  • 程式碼3 連結串列的節點
class Node<E> {

    E item;
    Node<E> next;
    
    //建構函式
    Node(E element) {
       this.item = element;
       this.next = null;
   }
}
  • 程式碼4 定義好節點後,使用前一般是對頭節點和尾節點進行初始化
//頭節點和尾節點都為空 連結串列為空
Node<E> head = null;
Node<E> tail = null;
  • 程式碼5 空連結串列建立一個新節點
//建立一個新的節點 並讓head指向此節點
head = new Node("nodedata1");

//讓尾節點也指向此節點
tail = head;
  • 程式碼6 連結串列追加一個節點
//建立新節點 同時和最後一個節點連線起來
tail.next = new Node("node1data2");

//尾節點指向新的節點
tail = tail.next;
  • 程式碼7 順序遍歷連結串列
Node<String> current = head;
while (current != null) {
    System.out.println(current.item);
    current = current.next;
}
  • 程式碼8 倒序遍歷連結串列
static void printListRev(Node<String> head) {
//倒序遍歷連結串列主要用了遞迴的思想
    if (head != null) {
        printListRev(head.next);
        System.out.println(head.item);
    }
}
  • 程式碼 單鏈表反轉
//單鏈表反轉 主要是逐一改變兩個節點間的連結關係來完成
static Node<String> revList(Node<String> head) {

    if (head == null) {
        return null;
    }

    Node<String> nodeResult = null;

    Node<String> nodePre = null;
    Node<String> current = head;

    while (current != null) {

        Node<String> nodeNext = current.next;

        if (nodeNext == null) {
            nodeResult = current;
        }

        current.next = nodePre;
        nodePre = current;
        current = nodeNext;
    }

    return nodeResult;
}

上面的幾段程式碼主要展示了連結串列的幾個基本操作,還有很多像獲取指定元素,移除元素等操作大家可以自己完成,寫這些程式碼的時候一定要理清節點之間關係,這樣才不容易出錯。

連結串列的實現還有其它的方式,常見的有迴圈單鏈表,雙向連結串列,迴圈雙向連結串列。 迴圈單鏈表 主要是連結串列的最後一個節點指向第一個節點,整體構成一個鏈環。 雙向連結串列 主要是節點中包含兩個指標部分,一個指向前驅元,一個指向後繼元,JDK中LinkedList集合類的實現就是雙向連結串列。** 迴圈雙向連結串列** 是最後一個節點指向第一個節點。

二、棧與佇列

棧和佇列也是比較常見的資料結構,它們是比較特殊的線性表,因為對於棧來說,訪問、插入和刪除元素只能在棧頂進行,對於佇列來說,元素只能從佇列尾插入,從佇列頭訪問和刪除。

棧是限制插入和刪除只能在一個位置上進行的表,該位置是表的末端,叫作棧頂,對棧的基本操作有push(進棧)和pop(出棧),前者相當於插入,後者相當於刪除最後一個元素。棧有時又叫作LIFO(Last In First Out)表,即後進先出。

棧的模型

下面我們看一道經典題目,加深對棧的理解。

關於棧的一道經典題目

上圖中的答案是C,其中的原理可以好好想一想。

因為棧也是一個表,所以任何實現表的方法都能實現棧。我們開啟JDK中的類Stack的原始碼,可以看到它就是繼承類Vector的。當然,Stack是Java2前的容器類,現在我們可以使用LinkedList來進行棧的所有操作。

佇列

佇列是一種特殊的線性表,特殊之處在於它只允許在表的前端(front)進行刪除操作,而在表的後端(rear)進行插入操作,和棧一樣,佇列是一種操作受限制的線性表。進行插入操作的端稱為隊尾,進行刪除操作的端稱為隊頭。

佇列示意圖

我們可以使用連結串列來實現佇列,下面程式碼簡單展示了利用LinkedList來實現佇列類。

  • 程式碼9 簡單實現佇列類
public class MyQueue<E> {

    private LinkedList<E> list = new LinkedList<>();

    // 入隊
    public void enqueue(E e) {
        list.addLast(e);
    }

    // 出隊
    public E dequeue() {
        return list.removeFirst();
    }
}

普通的佇列是一種先進先出的資料結構,而優先佇列中,元素都被賦予優先順序。當訪問元素的時候,具有最高優先順序的元素最先被刪除。優先佇列在生活中的應用還是比較多的,比如醫院的急症室為病人賦予優先順序,具有最高優先順序的病人最先得到治療。在Java集合框架中,類PriorityQueue就是優先佇列的實現類,具體大家可以去閱讀原始碼。

三、樹與二叉樹

樹型結構是一類非常重要的非線性資料結構,其中以樹和二叉樹最為常用。在介紹二叉樹之前,我們先簡單瞭解一下樹的相關內容。

** 樹 是由n(n>=1)個有限節點組成一個具有層次關係的集合。它具有以下特點:每個節點有零個或多個子節點;沒有父節點的節點稱為節點;每一個非根節點有且只有一個 父節點 **;除了根節點外,每個子節點可以分為多個不相交的子樹。

樹的結構

二叉樹基本概念

  • 定義

二叉樹是每個節點最多有兩棵子樹的樹結構。通常子樹被稱作“左子樹”和“右子樹”。二叉樹常被用於實現二叉查詢樹和二叉堆。

  • 相關性質

二叉樹的每個結點至多隻有2棵子樹(不存在度大於2的結點),二叉樹的子樹有左右之分,次序不能顛倒。

二叉樹的第i層至多有2(i-1)個結點;深度為k的二叉樹至多有2k-1個結點。

一棵深度為k,且有2^k-1個節點的二叉樹稱之為** 滿二叉樹 **;

深度為k,有n個節點的二叉樹,當且僅當其每一個節點都與深度為k的滿二叉樹中,序號為1至n的節點對應時,稱之為** 完全二叉樹 **。

  • 三種遍歷方法

在二叉樹的一些應用中,常常要求在樹中查詢具有某種特徵的節點,或者對樹中全部節點進行某種處理,這就涉及到二叉樹的遍歷。二叉樹主要是由3個基本單元組成,根節點、左子樹和右子樹。如果限定先左後右,那麼根據這三個部分遍歷的順序不同,可以分為先序遍歷、中序遍歷和後續遍歷三種。

(1) 先序遍歷 若二叉樹為空,則空操作,否則先訪問根節點,再先序遍歷左子樹,最後先序遍歷右子樹。 (2) 中序遍歷 若二叉樹為空,則空操作,否則先中序遍歷左子樹,再訪問根節點,最後中序遍歷右子樹。(3) 後序遍歷 若二叉樹為空,則空操作,否則先後序遍歷左子樹訪問根節點,再後序遍歷右子樹,最後訪問根節點。

給定二叉樹寫出三種遍歷結果
  • 樹和二叉樹的區別

(1) 二叉樹每個節點最多有2個子節點,樹則無限制。 (2) 二叉樹中節點的子樹分為左子樹和右子樹,即使某節點只有一棵子樹,也要指明該子樹是左子樹還是右子樹,即二叉樹是有序的。 (3) 樹決不能為空,它至少有一個節點,而一棵二叉樹可以是空的。

上面我們主要對二叉樹的相關概念進行了介紹,下面我們將從二叉查詢樹開始,介紹二叉樹的幾種常見型別,同時將之前的理論部分用程式碼實現出來。

二叉查詢樹

  • 定義

二叉查詢樹就是二叉排序樹,也叫二叉搜尋樹。二叉查詢樹或者是一棵空樹,或者是具有下列性質的二叉樹: (1) 若左子樹不空,則左子樹上所有結點的值均小於它的根結點的值;(2) 若右子樹不空,則右子樹上所有結點的值均大於它的根結點的值;(3) 左、右子樹也分別為二叉排序樹;(4) 沒有鍵值相等的結點。

典型的二叉查詢樹的構建過程
  • 效能分析

對於二叉查詢樹來說,當給定值相同但順序不同時,所構建的二叉查詢樹形態是不同的,下面看一個例子。

不同形態平衡二叉樹的ASL不同

可以看到,含有n個節點的二叉查詢樹的平均查詢長度和樹的形態有關。最壞情況下,當先後插入的關鍵字有序時,構成的二叉查詢樹蛻變為單支樹,樹的深度為n,其平均查詢長度(n+1)/2(和順序查詢相同),最好的情況是二叉查詢樹的形態和折半查詢的判定樹相同,其平均查詢長度和log2(n)成正比。平均情況下,二叉查詢樹的平均查詢長度和logn是等數量級的,所以為了獲得更好的效能,通常在二叉查詢樹的構建過程需要進行“平衡化處理”,之後我們將介紹平衡二叉樹和紅黑樹,這些均可以使查詢樹的高度為O(log(n))。

  • 程式碼10 二叉樹的節點

class TreeNode<E> {

    E element;
    TreeNode<E> left;
    TreeNode<E> right;

    public TreeNode(E e) {
        element = e;
    }
}

二叉查詢樹的三種遍歷都可以直接用遞迴的方法來實現:

  • 程式碼12 先序遍歷
protected void preorder(TreeNode<E> root) {

    if (root == null)
        return;

    System.out.println(root.element + " ");

    preorder(root.left);

    preorder(root.right);
}
  • 程式碼13 中序遍歷
protected void inorder(TreeNode<E> root) {

    if (root == null)
        return;

    inorder(root.left);

    System.out.println(root.element + " ");

    inorder(root.right);
}
  • 程式碼14 後序遍歷
protected void postorder(TreeNode<E> root) {

    if (root == null)
        return;

    postorder(root.left);

    postorder(root.right);

    System.out.println(root.element + " ");
}
  • 程式碼15 二叉查詢樹的簡單實現
/**
 * @author JackalTsc
 */
public class MyBinSearchTree<E extends Comparable<E>> {

    // 根
    private TreeNode<E> root;

    // 預設建構函式
    public MyBinSearchTree() {
    }

    // 二叉查詢樹的搜尋
    public boolean search(E e) {

        TreeNode<E> current = root;

        while (current != null) {

            if (e.compareTo(current.element) < 0) {
                current = current.left;
            } else if (e.compareTo(current.element) > 0) {
                current = current.right;
            } else {
                return true;
            }
        }

        return false;
    }

    // 二叉查詢樹的插入
    public boolean insert(E e) {

        // 如果之前是空二叉樹 插入的元素就作為根節點
        if (root == null) {
            root = createNewNode(e);
        } else {
            // 否則就從根節點開始遍歷 直到找到合適的父節點
            TreeNode<E> parent = null;
            TreeNode<E> current = root;
            while (current != null) {
                if (e.compareTo(current.element) < 0) {
                    parent = current;
                    current = current.left;
                } else if (e.compareTo(current.element) > 0) {
                    parent = current;
                    current = current.right;
                } else {
                    return false;
                }
            }
            // 插入
            if (e.compareTo(parent.element) < 0) {
                parent.left = createNewNode(e);
            } else {
                parent.right = createNewNode(e);
            }
        }
        return true;
    }

    // 建立新的節點
    protected TreeNode<E> createNewNode(E e) {
        return new TreeNode(e);
    }

}

// 二叉樹的節點
class TreeNode<E extends Comparable<E>> {

    E element;
    TreeNode<E> left;
    TreeNode<E> right;

    public TreeNode(E e) {
        element = e;
    }
}

上面的程式碼15主要展示了一個自己實現的簡單的二叉查詢樹,其中包括了幾個常見的操作,當然更多的操作還是需要大家自己去完成。因為在二叉查詢樹中刪除節點的操作比較複雜,所以下面我詳細介紹一下這裡。

  • 二叉查詢樹中刪除節點分析

要在二叉查詢樹中刪除一個元素,首先需要定位包含該元素的節點,以及它的父節點。假設current指向二叉查詢樹中包含該元素的節點,而parent指向current節點的父節點,current節點可能是parent節點的左孩子,也可能是右孩子。這裡需要考慮兩種情況:

  1. current節點沒有左孩子,那麼只需要將patent節點和current節點的右孩子相連。
  2. current節點有一個左孩子,假設rightMost指向包含current節點的左子樹中最大元素的節點,而parentOfRightMost指向rightMost節點的父節點。那麼先使用rightMost節點中的元素值替換current節點中的元素值,將parentOfRightMost節點和rightMost節點的左孩子相連,然後刪除rightMost節點。
    // 二叉搜尋樹刪除節點
    public boolean delete(E e) {

        TreeNode<E> parent = null;
        TreeNode<E> current = root;

        // 找到要刪除的節點的位置
        while (current != null) {
            if (e.compareTo(current.element) < 0) {
                parent = current;
                current = current.left;
            } else if (e.compareTo(current.element) > 0) {
                parent = current;
                current = current.right;
            } else {
                break;
            }
        }

        // 沒找到要刪除的節點
        if (current == null) {
            return false;
        }

        // 考慮第一種情況
        if (current.left == null) {
            if (parent == null) {
                root = current.right;
            } else {
                if (e.compareTo(parent.element) < 0) {
                    parent.left = current.right;
                } else {
                    parent.right = current.right;
                }
            }
        } else { // 考慮第二種情況
            TreeNode<E> parentOfRightMost = current;
            TreeNode<E> rightMost = current.left;
            // 找到左子樹中最大的元素節點
            while (rightMost.right != null) {
                parentOfRightMost = rightMost;
                rightMost = rightMost.right;
            }

            // 替換
            current.element = rightMost.element;

            // parentOfRightMost和rightMost左孩子相連
            if (parentOfRightMost.right == rightMost) {
                parentOfRightMost.right = rightMost.left;
            } else {
                parentOfRightMost.left = rightMost.left;
            }
        }

        return true;
    }

平衡二叉樹

平衡二叉樹又稱AVL樹,它或者是一棵空樹,或者是具有下列性質的二叉樹:它的左子樹和右子樹都是平衡二叉樹,且左子樹和右子樹的深度之差的絕對值不超過1。

平衡二叉樹

AVL樹是最先發明的自平衡二叉查詢樹演算法。在AVL中任何節點的兩個兒子子樹的高度最大差別為1,所以它也被稱為高度平衡樹,n個結點的AVL樹最大深度約1.44log2n。查詢、插入和刪除在平均和最壞情況下都是O(log n)。增加和刪除可能需要通過一次或多次樹旋轉來重新平衡這個樹。

紅黑樹

紅黑樹是平衡二叉樹的一種,它保證在最壞情況下基本動態集合操作的事件複雜度為O(log n)。紅黑樹和平衡二叉樹區別如下:(1) 紅黑樹放棄了追求完全平衡,追求大致平衡,在與平衡二叉樹的時間複雜度相差不大的情況下,保證每次插入最多隻需要三次旋轉就能達到平衡,實現起來也更為簡單。(2) 平衡二叉樹追求絕對平衡,條件比較苛刻,實現起來比較麻煩,每次插入新節點之後需要旋轉的次數不能預知。點選檢視更多

四、圖

  • 簡介

圖是一種較線性表和樹更為複雜的資料結構,線上性表中,資料元素之間僅有線性關係,在樹形結構中,資料元素之間有著明顯的層次關係,而在圖形結構中,節點之間的關係可以是任意的,圖中任意兩個資料元素之間都可能相關。圖的應用相當廣泛,特別是近年來的迅速發展,已經滲入到諸如語言學、邏輯學、物理、化學、電訊工程、電腦科學以及數學的其他分支中。

  • 相關閱讀

因為圖這部分的內容還是比較多的,這裡就不詳細介紹了,有需要的可以自己搜尋相關資料。

五、總結

到這裡,關於常見的資料結構的整理就結束了,斷斷續續大概花了兩天時間寫完,在總結的過程中,通過查閱相關資料,結合書本內容,收穫還是很大的,在下一篇部落格中將會介紹常用資料結構與演算法整理總結(下)之演算法篇,歡迎大家關注。