程序員面試闖關(guān)(二):數(shù)據(jù)結(jié)構(gòu)考點與細節(jié)分析

上一篇文章程序員面試闖關(guān)(一):字符串匹配+排序+查找列舉說明了各種常見的排序算法等算法基礎(chǔ),這里,主要分析下數(shù)據(jù)結(jié)構(gòu)相關(guān)的基礎(chǔ)和注意點。

一、線性表

1. 數(shù)組(順序存儲結(jié)構(gòu))

  1. 效率分析
  • 查找:O(1)【數(shù)組的存儲是連續(xù)內(nèi)存空間】
  • 插入和刪除:
    • 最好情況:O(1)
      ①插入到最后一個位置 ②刪除最后一個元素
    • 最壞情況:O(n)
      ①插入到第一個位置 ②刪除第一個元素
    • 平均情況:O((n-1)/2) = O(n)
  1. 整體分析:
    • 適合:存數(shù)據(jù),取數(shù)據(jù)【無須為了表示表中元素之間的邏輯關(guān)系而增加額外的存儲空間,可以快速地存取表中任一位置的元素】
    • 不適合:插入和刪除數(shù)據(jù)【需要頻繁移動元素】

2. 鏈表(鏈式存儲結(jié)構(gòu))

  1. 頭指針與頭結(jié)點的異同:
  • 頭指針:
    • 是指鏈表指向第一個結(jié)點的指針,若鏈表有頭結(jié)點,則是指向頭結(jié)點的指針。
    • 頭指針具有標識作用,所以常用它來冠以鏈表的名字
    • 無論鏈表是否為空,頭指針均不為空。頭指針是鏈表的必要元素。
  • 頭結(jié)點:
    • 頭結(jié)點是為了操作的統(tǒng)一和方便而設(shè)定的,放在第一個元素的結(jié)點之前,其數(shù)據(jù)域一般無意義(一般用于存放鏈表的長度)
    • 有了頭結(jié)點,對在第一元素結(jié)點前插入結(jié)點和刪除第一結(jié)點,其操作與其他結(jié)點的操作就統(tǒng)一。
    • 頭結(jié)點不一定是鏈表的必要元素。
  1. 效率分析
  • 查找:O(n)
  • 插入和刪除:O(1)
  1. 優(yōu)勢:
  • 相比于數(shù)組:數(shù)組需要預(yù)先分配存儲空間,容易造成空間浪費和溢出。而單鏈表不需要分配空間,元素個數(shù)也不受限制。
  1. 細分
  • 單鏈表:就是上述這種,內(nèi)存空間分配不連續(xù)。
  • 靜態(tài)鏈表:【就是為了給沒有指針的語言設(shè)計的方案
    】用數(shù)組去模擬鏈表,內(nèi)存空間。具體實現(xiàn):設(shè)計一個結(jié)構(gòu)體,創(chuàng)建一個結(jié)構(gòu)體數(shù)組,這個數(shù)組很長,足夠你用了,然后,用兩個結(jié)構(gòu)體實例,一個作為數(shù)組頭,一個作為數(shù)組尾,用于說明實際的數(shù)組長度:
    typedef struct{
      ElemType data;
      int cur; //表示下一個元素的下標(一般會指向它下一個位置,如果是末尾,則指向‘0’)
    }
    
    • 優(yōu)點:要插入或刪除元素,只需要先在數(shù)組末尾添加這個元素,然后,修改這個元素的cur和插入點元素的cur即可。避免了元素移位的復雜過程。
    • 缺點:表長度難以確定的問題;失去了數(shù)組的隨機存取快的特性。
  • 循環(huán)鏈表
  • 雙向鏈表

二、棧和隊列

1. 棧

  • 概念:棧是限定僅在表尾進行插入和刪除操作的線性表。
  • 結(jié)構(gòu):
    1. 順序結(jié)構(gòu)(一個棧):
    typedef struct{
      int data[MAXSIZE];
      int top;   //表示棧頂下標(-1表示:棧空)
    }Stack
    
    1. 順序結(jié)構(gòu)(兩個棧共享空間):
    typedef struct{
      int data[MAXSIZE];
      int topA;   //表示棧A的棧頂下標(-1表示:??眨?  int topB;   //表示棧B的棧頂下標(MAXSIZE 表示:棧空)
    }DoubleStack
    
    1. 鏈式結(jié)構(gòu):
    typedef struct StackNode{///棧元素
      int data;
      struct StackNode *next; 
    }StackNode , *LinkStackPtr;
    
    typedef struct LinkStack{//鏈棧
      LinkStackPtr top;///指向棧頂?shù)闹羔?  int count;//棧大?。?表示:空棧)
    }LinkStack;
    

2. 隊列

  • 概念:隊列是只允許一端進行插入操作,另一端進行刪除操作的線性表。
  • 循環(huán)隊列
    • 目的:為了解決存儲空間的循環(huán)利用的問題。
    • 性質(zhì):
      1. 隊列滿的條件是:(rear+1)%QueueSize == front 【其中‘rear’表示隊尾,‘font’表示隊頭】
      2. 結(jié)構(gòu)如下:
      typedef struct{
        int data[MAXSIZE];
        int front;  //頭指針(初始化為:0)
        int rear;  //尾指針(初始化為:0,隊列不空,就指向隊列最后一個元素的下一個位置)
      }
      
    • 操作:
      1. 入隊:
      Q->rear = (Q->rear + 1)%MAXSIZE;
      
      1. 出隊:
      Q->front = (Q->front + 1)%MAXSIZE;
      
  • 鏈隊列
    • 結(jié)構(gòu):
      typedef struct QNode{
        int data;
        struct QNode *next;
      }QNode , *QueuePtr;
      
      typedef struct{
        QueuePtr front, rear; /*隊頭,隊尾指針*/
      }
      

三、樹

1. 各種概念重溫

  • 結(jié)點層次(Level):根為第一層,根的孩子為第二層。
  • 其雙親在同一層的結(jié)點互為堂兄弟。
  • 森林:m(m>=0)課互不相交的樹的集合。

2. 一般樹的存儲結(jié)構(gòu)

  • 雙親表示法【缺點:找父親O(1),找孩子O(n)】
    其實是對象數(shù)組(包含其父結(jié)點的下標)


    typedef struct PTNode{
      int data;
      int parent;//父結(jié)點的下標,‘-1’表示沒有父結(jié)點,本身是根結(jié)點
    }
    typedef struct{
      PTNode nodes[100];  //結(jié)點數(shù)組
      int r, n;  //根位置 , 結(jié)點數(shù)
    }
    
  • 孩子表示法【缺點:①找父親O(n),找孩子O(1) ②要加孩子,結(jié)構(gòu)需要改,遍歷更加復雜】
    其實是將n個結(jié)點用一個長度為n的結(jié)點數(shù)組裝起來,每個結(jié)點的子結(jié)點用一條屬于這個結(jié)點的單鏈表來表示,如果是葉子結(jié)點,那么則沒有子鏈表,它的firstchild為null。


    typedef struct CTNode{  ///孩子結(jié)點
      int child;
      struct CTNode *next;
    } *ChildPtr;
    typedef struct{    ////表頭結(jié)構(gòu)
      int data;
      ChildPtr firstchild;
    } CTBox;
    typedef struct{   /////樹結(jié)構(gòu)
      CTBox nodes[100]; //結(jié)點數(shù)組
      int r,n;  ///根位置和結(jié)點數(shù)
    }
    
  • 孩子兄弟表示法【缺點,還是難找到父結(jié)點 ,如果要改,需要加多一個指針】
    一個結(jié)構(gòu)體,兩個指針(一個縱向,一個橫向),相當于他的“長子”和“二弟”。


    typedef struct CSNode{
      int data;
      struct CSNode *firstChild;///它的第一個孩子結(jié)點
      struct CSNode *rightChild;///它本身的兄弟結(jié)點
    }
    

3. 二叉樹

  • 相關(guān)概念:

    • 斜樹:所有結(jié)點都在左子樹 稱為左斜樹;所有結(jié)點都在右子樹 稱為右斜樹。
    • 完全二叉樹的特點:
      1. 葉子結(jié)點只能在最下兩層。
      2. 最下層葉子一定集中在左部連續(xù)位置。
      3. 如果結(jié)點度數(shù)為1,此結(jié)點必定只有左子樹。
      4. 同樣結(jié)點數(shù)的二叉樹,完全二叉樹的深度最小。
    • 二叉樹的性質(zhì):
      1. 在二叉樹的第i層上至多有2^(i-1)個結(jié)點
      2. 深度為k的二叉樹至多有2^k -1個結(jié)點
      3. 任意一顆二叉樹,n2(度數(shù)為2的結(jié)點) = n0(度數(shù)為0的結(jié)點)-1
      4. 樹的邊數(shù)(也叫:分支線總數(shù))和結(jié)點數(shù)的關(guān)系:邊數(shù) = n-1 = n1+2*n2
      5. 具有n個結(jié)點的完全二叉樹的深度為

        表示:不大于logn的最大整數(shù)+1

      6. 如果放在一個數(shù)組中,那么,父子結(jié)點之間的下標關(guān)系如下:


  • 二叉樹相關(guān)操作

  • 線索二叉樹

    • 定義:因為二叉鏈表只能知道某個結(jié)點的左右孩子結(jié)點的地址,而不知道它在遍歷時的一個‘前驅(qū)’和‘后繼’結(jié)點是誰。于是,將原來的結(jié)點結(jié)構(gòu)中的左右孩子成員,在判斷為null的情況下, 變?yōu)檫@個結(jié)點的前驅(qū)和后繼結(jié)點。這樣,一來充分利用了空間,二來方便了我們查找某個結(jié)點的前驅(qū)和后繼結(jié)點。
    • 結(jié)構(gòu):
      typedef enum {LInk , Thread} PointerTag; //Link == 0 表示指向左右孩子,Thread==1 表示指向前驅(qū)和后繼結(jié)點
      typedef struct BiThrNode{
        int data; //數(shù)據(jù)
        struct BiThrNode *lchild , *rchild;//左右孩子指針(或者前驅(qū)后繼結(jié)點)
        PointerTag LTag;  //左標志
        PointerTag RTag;  //右標志
      } BiThrNode, *BiThrTree;
      
    • 中序遍歷示例


    • 意義
      如果實際中所用的二叉樹需要經(jīng)常遍歷或查找結(jié)點時需要某種遍歷序列中的前驅(qū)和后繼,那么采用線索二叉鏈表的存儲結(jié)構(gòu)是不錯選擇。
  • 二叉樹、數(shù)、森林之間的轉(zhuǎn)換

    1. 樹轉(zhuǎn)二叉樹:
    2. 森林轉(zhuǎn)二叉樹:


    3. 二叉樹轉(zhuǎn)樹:
    4. 二叉樹轉(zhuǎn)森林:
  • 哈夫曼樹
    從樹中一個結(jié)點到另一個結(jié)點之間的分支構(gòu)成兩個結(jié)點之間的路徑,路徑上的分支數(shù)目稱作 路徑長度。【帶權(quán)路徑長度WPL最小的二叉樹,稱作 赫夫曼樹】

四、圖

1. 定義:G(V【頂點集合】, E【邊的集合】)

2. 相關(guān)概念

  1. 完全圖:任意兩個點之間有邊的圖
  2. 簡單圖:無重復的邊或頂點到自身的邊
  3. 度:頂點的邊數(shù)

3. 圖的存儲結(jié)構(gòu)

  • 鄰接矩陣
    • 存儲方式:用兩個數(shù)組來表示圖。一個一維數(shù)組存儲圖中頂點信息,一個二維數(shù)組存儲圖中的邊或弧的信息。
    • 優(yōu)勢:
      1. 容易判斷兩個頂點是否有邊
      2. 要知道某個頂點的度,其實就是這個頂點vi 在鄰接矩陣中的第i行(或第i列)的元素之和。
      3. 求頂點vi的所有鄰接點,只需要遍歷矩陣第i行。
    • 圖解(無向連通圖為例):
  • 鄰接表
    • 存儲方式:用兩個對象結(jié)構(gòu),一個來表示頂點,另一個則表示該頂點的鄰接頂點權(quán)值信息。每個頂點對象,都是一個鏈表的表頭結(jié)點,他后面接著的是他的所有鄰接頂點的信息列表。如下圖,表示每一個頂點的鏈表結(jié)構(gòu)模型。


      每一個頂點的鄰接表

鄰接矩陣和鄰接表的比較:

  • 當圖中結(jié)點數(shù)目較小且邊較多時,采用鄰接矩陣效率更高。
  • 當節(jié)點數(shù)目遠大且邊的數(shù)目遠小于相同結(jié)點的完全圖的邊數(shù)時,采用鄰接表存儲結(jié)構(gòu)更有效率。

五、二叉樹和圖的各個??挤椒ùa匯總(代碼可直接復制運行)

1. 二叉樹各個方法

package basic.tree;

import java.util.ArrayDeque;
import java.util.Queue;
import java.util.Scanner;
import java.util.Stack;

/**
 * 二叉樹
 * Created by androidjp on 16/8/17.
 */
public class BiTree {
    public String data;
    public BiTree left;
    public BiTree right;

    public static Scanner scanner = new Scanner(System.in);

    /**
     * 以 先序遍歷 的順序進行元素插入
     *
     * @return 創(chuàng)建好的二叉樹
     */
    public static BiTree createTree() {
        String s = scanner.next();
        char ch = s.charAt(0);
        if (ch == '#') {
            return null;
        }
        BiTree root = new BiTree();
        root.data = s;
        root.left = null;
        root.right = null;

        //遞歸進行插入
        root.left = createTree();
        root.right = createTree();

        return root;
    }

    /**
     * 先序遍歷二叉樹
     *
     * @param tree
     */
    public static void preTraverse(BiTree tree) {
        if (tree == null)
            return;
        System.out.print(tree.data + " ");
        preTraverse(tree.left);
        preTraverse(tree.right);

    }

    /**
     * 中序遍歷二叉樹
     *
     * @param tree
     */
    public static void orderTraverse(BiTree tree) {
        if (tree == null)
            return;
        orderTraverse(tree.left);
        System.out.print(tree.data +" ");
        orderTraverse(tree.right);

    }

    /**
     * 后序遍歷二叉樹
     *
     * @param tree
     */
    public static void postTraverse(BiTree tree) {
        if (tree == null)
            return;
        postTraverse(tree.left);
        postTraverse(tree.right);
        System.out.print(tree.data + " ");
    }

    /**
     * 二叉樹的深度優(yōu)先遍歷
     * 【二叉樹不同于圖,圖需要標記元素是否已經(jīng)被遍歷過,因為可能存在環(huán),而樹則不用考慮環(huán)的問題,于是少了判斷這一步】
     * 使用棧 遍歷
     *
     * @param tree
     */
    public static void depthFirstTraverse(BiTree tree) {
        Stack<BiTree> stack = new Stack<>();
        stack.push(tree);
        while (!stack.empty()) {
            tree = stack.peek();
            System.out.print(tree.data + " ");
            stack.pop();
            //注意 元素入棧先后順序: right -> left
            if (tree.right!= null){
                stack.push(tree.right);
            }
            if (tree.left != null) {
                stack.push(tree.left);
            }
        }
    }

    /**
     * 廣度優(yōu)先遍歷(也稱為:層次遍歷)
     * 利用隊列
     * @param tree
     */
    public static void breadFirstTraverse(BiTree tree){
        Queue<BiTree> queue = new ArrayDeque<>();
        queue.add(tree);
        while(!queue.isEmpty()){
            tree = queue.poll();
            System.out.print(tree.data + " ");
            if (tree.left!=null){
                queue.add(tree.left);
            }
            if (tree.right!=null){
                queue.add(tree.right);
            }
        }
    }


    public static void main(String[] args) {
        BiTree tree = new BiTree();
        ///(根節(jié)點 -> 左孩子 -> 右孩子)創(chuàng)建二叉樹,如輸入: 1 2a 3a # 4b # # 3b # # 2b # #
        tree = createTree();
        BiTree ptr = tree;

        //先序遍歷
        preTraverse(ptr);
        System.out.println();

        //中序遍歷
        ptr = tree;
        orderTraverse(ptr);
        System.out.println();

        //后序遍歷
        ptr = tree;
        postTraverse(ptr);
        System.out.println();

        //深度優(yōu)先遍歷
        ptr = tree;
        depthFirstTraverse(ptr);
        System.out.println();

        //廣度優(yōu)先遍歷
        ptr = tree;
        breadFirstTraverse(ptr);
        System.out.println();
    }
}

2. 圖的廣度和深度優(yōu)先比遍歷(鄰接矩陣表示權(quán)值)

import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;

/**
 * 使用鄰接矩陣來表示'邊'的圖結(jié)構(gòu)
 * Created by androidjp on 16/9/8.
 */
public class MyGraph<V> {
    private ArrayList<V> vertexList;//點集合
    private int[][] edges; //鄰接矩陣表示的邊集合
    private int numOfEdges; //邊數(shù)

    public MyGraph(int num){
        vertexList  = new ArrayList(num);
        edges = new int[num][num];
        numOfEdges = 0;
    }

    ///邊數(shù)
    public int getNumOfEdges(){
        return this.numOfEdges;
    }

    ///結(jié)點數(shù)
    public int getNumOfVertex(){
        return this.vertexList.size();
    }

    //獲取第i個結(jié)點
    public V getVertex(int i){
        return this.vertexList.get(i);
    }

    //獲取i、j兩個結(jié)點之間的權(quán)值
    public int getWeight(int i, int j){
        return this.edges[i][j];
    }

    //插入結(jié)點
    public void insertVertex(V vertex) {
        vertexList.add(vertexList.size(),vertex);
    }

    //插入結(jié)點
    public void insertEdge(int v1,int v2,int weight) {
        edges[v1][v2]=weight;
        numOfEdges++;
    }

    //刪除結(jié)點
    public void deleteEdge(int v1,int v2) {
        edges[v1][v2]=0;
        numOfEdges--;
    }

    //得到第一個鄰接結(jié)點的下標
    public int getFirstNeighbor(int index) {
        for(int j=0;j<vertexList.size();j++) {
            if (edges[index][j]>0) {
                return j;
            }
        }
        return -1;
    }

    //根據(jù)前一個鄰接結(jié)點的下標來取得下一個鄰接結(jié)點
    public int getNextNeighbor(int v1,int v2) {
        for (int j=v2+1;j<vertexList.size();j++) {
            if (edges[v1][j]>0) {
                return j;
            }
        }
        return -1;
    }

    //私有函數(shù),深度優(yōu)先遍歷
    private void depthFirstSearch(boolean[] isVisited,int  i) {
        //首先訪問該結(jié)點,在控制臺打印出來
        System.out.print(getVertex(i)+"  ");
        //置該結(jié)點為已訪問
        isVisited[i]=true;

        int w=getFirstNeighbor(i);
        while (w!=-1) {
            if (!isVisited[w]) {
                depthFirstSearch(isVisited,w);
            }
            w=getNextNeighbor(i, w);
        }
    }

    //對外公開函數(shù),深度優(yōu)先遍歷,與其同名私有函數(shù)屬于方法重載
    public void depthFirstSearch() {
        boolean[] isVisited = new boolean[getNumOfVertex()];
        for(int i=0;i<isVisited.length;i++)
            isVisited[i] = false;

        for(int i=0;i<getNumOfVertex();i++) {
            //因為對于非連通圖來說,并不是通過一個結(jié)點就一定可以遍歷所有結(jié)點的。
            if (!isVisited[i]) {
                depthFirstSearch(isVisited,i);
            }
        }
    }

    //私有函數(shù),廣度優(yōu)先遍歷
    private void broadFirstSearch(boolean[] isVisited,int i) {
        int u,w;
        LinkedList queue=new LinkedList();

        //訪問結(jié)點i
        System.out.print(getVertex(i)+"  ");
        isVisited[i]=true;
        //結(jié)點入隊列
        queue.addLast(i);
        while (!queue.isEmpty()) {
            u=((Integer)queue.removeFirst()).intValue();
            w=getFirstNeighbor(u);
            while(w!=-1) {
                if(!isVisited[w]) {
                    //訪問該結(jié)點
                    System.out.print(getVertex(w)+"  ");
                    //標記已被訪問
                    isVisited[w]=true;
                    //入隊列
                    queue.addLast(w);
                }
                //尋找下一個鄰接結(jié)點
                w=getNextNeighbor(u, w);
            }
        }
    }

    //對外公開函數(shù),廣度優(yōu)先遍歷
    public void broadFirstSearch() {
        boolean[] isVisited = new boolean[getNumOfVertex()];
        for(int i=0;i<isVisited.length;i++)
            isVisited[i] = false;
        for(int i=0;i<getNumOfVertex();i++) {
            if(!isVisited[i]) {
                broadFirstSearch(isVisited, i);
            }
        }
    }



    public static void main(String args[]) {
        int n = 8, e = 9;//分別代表結(jié)點個數(shù)和邊的數(shù)目
        String labels[] = {"1", "2", "3", "4", "5", "6", "7", "8"};//結(jié)點的標識
        MyGraph<String> graph = new MyGraph<String>(n);
        for (String label : labels) {
            graph.insertVertex(label);//插入結(jié)點
        }
        //插入九條邊
        graph.insertEdge(0, 1, 1);
        graph.insertEdge(0, 2, 1);
        graph.insertEdge(1, 3, 1);
        graph.insertEdge(1, 4, 1);
        graph.insertEdge(3, 7, 1);
        graph.insertEdge(4, 7, 1);
        graph.insertEdge(2, 5, 1);
        graph.insertEdge(2, 6, 1);
        graph.insertEdge(5, 6, 1);
        graph.insertEdge(1, 0, 1);
        graph.insertEdge(2, 0, 1);
        graph.insertEdge(3, 1, 1);
        graph.insertEdge(4, 1, 1);
        graph.insertEdge(7, 3, 1);
        graph.insertEdge(7, 4, 1);
        graph.insertEdge(6, 2, 1);
        graph.insertEdge(5, 2, 1);
        graph.insertEdge(6, 5, 1);

        System.out.println("深度優(yōu)先搜索序列為:");
        graph.depthFirstSearch();
        System.out.println();
        System.out.println("廣度優(yōu)先搜索序列為:");
        graph.broadFirstSearch();
    }
}
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容