上一篇文章程序員面試闖關(一):字符串匹配+排序+查找列舉說明了各種常見的排序算法等算法基礎,這里,主要分析下數據結構相關的基礎和注意點。
一、線性表
1. 數組(順序存儲結構)
- 效率分析
- 查找:O(1)【數組的存儲是連續內存空間】
- 插入和刪除:
- 最好情況:O(1)
①插入到最后一個位置 ②刪除最后一個元素
- 最壞情況:O(n)
①插入到第一個位置 ②刪除第一個元素
- 平均情況:O((n-1)/2) = O(n)
- 最好情況:O(1)
- 整體分析:
- 適合:存數據,取數據【無須為了表示表中元素之間的邏輯關系而增加額外的存儲空間,可以快速地存取表中任一位置的元素】
- 不適合:插入和刪除數據【需要頻繁移動元素】
2. 鏈表(鏈式存儲結構)
- 頭指針與頭結點的異同:
- 頭指針:
- 是指鏈表指向第一個結點的指針,若鏈表有頭結點,則是指向頭結點的指針。
- 頭指針具有標識作用,所以常用它來冠以鏈表的名字
- 無論鏈表是否為空,頭指針均不為空。頭指針是鏈表的必要元素。
- 頭結點:
- 頭結點是為了操作的統一和方便而設定的,放在第一個元素的結點之前,其數據域一般無意義(一般用于存放鏈表的長度)
- 有了頭結點,對在第一元素結點前插入結點和刪除第一結點,其操作與其他結點的操作就統一。
- 頭結點不一定是鏈表的必要元素。
- 效率分析
- 查找:O(n)
- 插入和刪除:O(1)
- 優勢:
- 相比于數組:數組需要預先分配存儲空間,容易造成空間浪費和溢出。而單鏈表不需要分配空間,元素個數也不受限制。
- 細分
- 單鏈表:就是上述這種,內存空間分配不連續。
-
靜態鏈表:【就是為了給沒有指針的語言設計的方案
】用數組去模擬鏈表,內存空間。具體實現:設計一個結構體,創建一個結構體數組,這個數組很長,足夠你用了,然后,用兩個結構體實例,一個作為數組頭,一個作為數組尾,用于說明實際的數組長度:typedef struct{ ElemType data; int cur; //表示下一個元素的下標(一般會指向它下一個位置,如果是末尾,則指向‘0’) }
- 優點:要插入或刪除元素,只需要先在數組末尾添加這個元素,然后,修改這個元素的cur和插入點元素的cur即可。避免了元素移位的復雜過程。
- 缺點:表長度難以確定的問題;失去了數組的隨機存取快的特性。
- 循環鏈表
- 雙向鏈表
二、棧和隊列
1. 棧
- 概念:棧是限定僅在表尾進行插入和刪除操作的線性表。
- 結構:
- 順序結構(一個棧):
typedef struct{ int data[MAXSIZE]; int top; //表示棧頂下標(-1表示:棧空) }Stack
- 順序結構(兩個棧共享空間):
typedef struct{ int data[MAXSIZE]; int topA; //表示棧A的棧頂下標(-1表示:棧空) int topB; //表示棧B的棧頂下標(MAXSIZE 表示:棧空) }DoubleStack
- 鏈式結構:
typedef struct StackNode{///棧元素 int data; struct StackNode *next; }StackNode , *LinkStackPtr; typedef struct LinkStack{//鏈棧 LinkStackPtr top;///指向棧頂的指針 int count;//棧大小(0表示:空棧) }LinkStack;
2. 隊列
- 概念:隊列是只允許一端進行插入操作,另一端進行刪除操作的線性表。
-
循環隊列:
- 目的:為了解決存儲空間的循環利用的問題。
- 性質:
- 隊列滿的條件是:(rear+1)%QueueSize == front 【其中‘rear’表示隊尾,‘font’表示隊頭】
- 結構如下:
typedef struct{ int data[MAXSIZE]; int front; //頭指針(初始化為:0) int rear; //尾指針(初始化為:0,隊列不空,就指向隊列最后一個元素的下一個位置) }
- 操作:
- 入隊:
Q->rear = (Q->rear + 1)%MAXSIZE;
- 出隊:
Q->front = (Q->front + 1)%MAXSIZE;
-
鏈隊列:
- 結構:
typedef struct QNode{ int data; struct QNode *next; }QNode , *QueuePtr; typedef struct{ QueuePtr front, rear; /*隊頭,隊尾指針*/ }
- 結構:
三、樹
1. 各種概念重溫
- 結點層次(Level):根為第一層,根的孩子為第二層。
- 其雙親在同一層的結點互為堂兄弟。
- 森林:m(m>=0)課互不相交的樹的集合。
2. 一般樹的存儲結構
-
雙親表示法【缺點:找父親O(1),找孩子O(n)】
其實是對象數組(包含其父結點的下標)
typedef struct PTNode{ int data; int parent;//父結點的下標,‘-1’表示沒有父結點,本身是根結點 } typedef struct{ PTNode nodes[100]; //結點數組 int r, n; //根位置 , 結點數 }
-
孩子表示法【缺點:①找父親O(n),找孩子O(1) ②要加孩子,結構需要改,遍歷更加復雜】
其實是將n個結點用一個長度為n的結點數組裝起來,每個結點的子結點用一條屬于這個結點的單鏈表來表示,如果是葉子結點,那么則沒有子鏈表,它的firstchild為null。
typedef struct CTNode{ ///孩子結點 int child; struct CTNode *next; } *ChildPtr; typedef struct{ ////表頭結構 int data; ChildPtr firstchild; } CTBox; typedef struct{ /////樹結構 CTBox nodes[100]; //結點數組 int r,n; ///根位置和結點數 }
-
孩子兄弟表示法【缺點,還是難找到父結點 ,如果要改,需要加多一個指針】
一個結構體,兩個指針(一個縱向,一個橫向),相當于他的“長子”和“二弟”。
typedef struct CSNode{ int data; struct CSNode *firstChild;///它的第一個孩子結點 struct CSNode *rightChild;///它本身的兄弟結點 }
3. 二叉樹
-
相關概念:
- 斜樹:所有結點都在左子樹 稱為左斜樹;所有結點都在右子樹 稱為右斜樹。
- 完全二叉樹的特點:
- 葉子結點只能在最下兩層。
- 最下層葉子一定集中在左部連續位置。
- 如果結點度數為1,此結點必定只有左子樹。
- 同樣結點數的二叉樹,完全二叉樹的深度最小。
- 二叉樹的性質:
- 在二叉樹的第i層上至多有2^(i-1)個結點
- 深度為k的二叉樹至多有2^k -1個結點
- 任意一顆二叉樹,n2(度數為2的結點) = n0(度數為0的結點)-1
- 樹的邊數(也叫:分支線總數)和結點數的關系:邊數 = n-1 = n1+2*n2
-
具有n個結點的完全二叉樹的深度為
表示:不大于logn的最大整數+1
-
如果放在一個數組中,那么,父子結點之間的下標關系如下:
二叉樹相關操作
-
線索二叉樹
- 定義:因為二叉鏈表只能知道某個結點的左右孩子結點的地址,而不知道它在遍歷時的一個‘前驅’和‘后繼’結點是誰。于是,將原來的結點結構中的左右孩子成員,在判斷為null的情況下, 變為這個結點的前驅和后繼結點。這樣,一來充分利用了空間,二來方便了我們查找某個結點的前驅和后繼結點。
- 結構:
typedef enum {LInk , Thread} PointerTag; //Link == 0 表示指向左右孩子,Thread==1 表示指向前驅和后繼結點 typedef struct BiThrNode{ int data; //數據 struct BiThrNode *lchild , *rchild;//左右孩子指針(或者前驅后繼結點) PointerTag LTag; //左標志 PointerTag RTag; //右標志 } BiThrNode, *BiThrTree;
-
中序遍歷示例
- 意義
如果實際中所用的二叉樹需要經常遍歷或查找結點時需要某種遍歷序列中的前驅和后繼,那么采用線索二叉鏈表的存儲結構是不錯選擇。
-
二叉樹、數、森林之間的轉換
-
樹轉二叉樹:
-
森林轉二叉樹:
-
二叉樹轉樹:
-
二叉樹轉森林:
-
樹轉二叉樹:
哈夫曼樹
從樹中一個結點到另一個結點之間的分支構成兩個結點之間的路徑,路徑上的分支數目稱作 路徑長度。【帶權路徑長度WPL最小的二叉樹,稱作 赫夫曼樹】
四、圖
1. 定義:G(V【頂點集合】, E【邊的集合】)
2. 相關概念
- 完全圖:任意兩個點之間有邊的圖
- 簡單圖:無重復的邊或頂點到自身的邊
- 度:頂點的邊數
3. 圖的存儲結構
- 鄰接矩陣
- 存儲方式:用兩個數組來表示圖。一個一維數組存儲圖中頂點信息,一個二維數組存儲圖中的邊或弧的信息。
- 優勢:
- 容易判斷兩個頂點是否有邊
- 要知道某個頂點的度,其實就是這個頂點vi 在鄰接矩陣中的第i行(或第i列)的元素之和。
- 求頂點vi的所有鄰接點,只需要遍歷矩陣第i行。
-
圖解(無向連通圖為例):
- 鄰接表
-
存儲方式:用兩個對象結構,一個來表示頂點,另一個則表示該頂點的鄰接頂點權值信息。每個頂點對象,都是一個鏈表的表頭結點,他后面接著的是他的所有鄰接頂點的信息列表。如下圖,表示每一個頂點的鏈表結構模型。
每一個頂點的鄰接表
-
鄰接矩陣和鄰接表的比較:
- 當圖中結點數目較小且邊較多時,采用鄰接矩陣效率更高。
- 當節點數目遠大且邊的數目遠小于相同結點的完全圖的邊數時,采用鄰接表存儲結構更有效率。
五、二叉樹和圖的各個常考方法代碼匯總(代碼可直接復制運行)
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 創建好的二叉樹
*/
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 + " ");
}
/**
* 二叉樹的深度優先遍歷
* 【二叉樹不同于圖,圖需要標記元素是否已經被遍歷過,因為可能存在環,而樹則不用考慮環的問題,于是少了判斷這一步】
* 使用棧 遍歷
*
* @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);
}
}
}
/**
* 廣度優先遍歷(也稱為:層次遍歷)
* 利用隊列
* @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();
///(根節點 -> 左孩子 -> 右孩子)創建二叉樹,如輸入: 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();
//深度優先遍歷
ptr = tree;
depthFirstTraverse(ptr);
System.out.println();
//廣度優先遍歷
ptr = tree;
breadFirstTraverse(ptr);
System.out.println();
}
}
2. 圖的廣度和深度優先比遍歷(鄰接矩陣表示權值)
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
/**
* 使用鄰接矩陣來表示'邊'的圖結構
* Created by androidjp on 16/9/8.
*/
public class MyGraph<V> {
private ArrayList<V> vertexList;//點集合
private int[][] edges; //鄰接矩陣表示的邊集合
private int numOfEdges; //邊數
public MyGraph(int num){
vertexList = new ArrayList(num);
edges = new int[num][num];
numOfEdges = 0;
}
///邊數
public int getNumOfEdges(){
return this.numOfEdges;
}
///結點數
public int getNumOfVertex(){
return this.vertexList.size();
}
//獲取第i個結點
public V getVertex(int i){
return this.vertexList.get(i);
}
//獲取i、j兩個結點之間的權值
public int getWeight(int i, int j){
return this.edges[i][j];
}
//插入結點
public void insertVertex(V vertex) {
vertexList.add(vertexList.size(),vertex);
}
//插入結點
public void insertEdge(int v1,int v2,int weight) {
edges[v1][v2]=weight;
numOfEdges++;
}
//刪除結點
public void deleteEdge(int v1,int v2) {
edges[v1][v2]=0;
numOfEdges--;
}
//得到第一個鄰接結點的下標
public int getFirstNeighbor(int index) {
for(int j=0;j<vertexList.size();j++) {
if (edges[index][j]>0) {
return j;
}
}
return -1;
}
//根據前一個鄰接結點的下標來取得下一個鄰接結點
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;
}
//私有函數,深度優先遍歷
private void depthFirstSearch(boolean[] isVisited,int i) {
//首先訪問該結點,在控制臺打印出來
System.out.print(getVertex(i)+" ");
//置該結點為已訪問
isVisited[i]=true;
int w=getFirstNeighbor(i);
while (w!=-1) {
if (!isVisited[w]) {
depthFirstSearch(isVisited,w);
}
w=getNextNeighbor(i, w);
}
}
//對外公開函數,深度優先遍歷,與其同名私有函數屬于方法重載
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++) {
//因為對于非連通圖來說,并不是通過一個結點就一定可以遍歷所有結點的。
if (!isVisited[i]) {
depthFirstSearch(isVisited,i);
}
}
}
//私有函數,廣度優先遍歷
private void broadFirstSearch(boolean[] isVisited,int i) {
int u,w;
LinkedList queue=new LinkedList();
//訪問結點i
System.out.print(getVertex(i)+" ");
isVisited[i]=true;
//結點入隊列
queue.addLast(i);
while (!queue.isEmpty()) {
u=((Integer)queue.removeFirst()).intValue();
w=getFirstNeighbor(u);
while(w!=-1) {
if(!isVisited[w]) {
//訪問該結點
System.out.print(getVertex(w)+" ");
//標記已被訪問
isVisited[w]=true;
//入隊列
queue.addLast(w);
}
//尋找下一個鄰接結點
w=getNextNeighbor(u, w);
}
}
}
//對外公開函數,廣度優先遍歷
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;//分別代表結點個數和邊的數目
String labels[] = {"1", "2", "3", "4", "5", "6", "7", "8"};//結點的標識
MyGraph<String> graph = new MyGraph<String>(n);
for (String label : labels) {
graph.insertVertex(label);//插入結點
}
//插入九條邊
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("深度優先搜索序列為:");
graph.depthFirstSearch();
System.out.println();
System.out.println("廣度優先搜索序列為:");
graph.broadFirstSearch();
}
}