C數據結構和使用詳情(基礎)

寫在前面

本篇繼上篇文章重點介紹數據結構的使用方法,主要針對不同的數據結構的創建、增刪改查等基礎操作,又根據每個數據結構的特點延伸出特色的其他使用方向。

0、數組

面試中常見問題:
1、尋找數組中第二小的元素
【解答】利用排序算法,先排序再遍歷找

2、找到數組中第一個不重複出現的整數參考文獻
【解答】這種對查找順序有要求的,不能先排序,雙循環查找(On2

hash表,(bitmap形式),循環一次,將所有的不重複的數找到,然後按照找第一個出現的數

3、找到數組中的第一個重複出現的整數
【解答】用hash表較好,記錄每一個出現出的索引,第二查找到發現對應的hash值大於零,則讀取索引即爲第一個重複數的

4、合併兩個有序數組
【解答】雙指針比較

5、重新排列數組中的正值和負值
【推薦解答】構建兩個堆空間,遍歷一次將正數和負數分別存入,再合併,需要空間較大

  • 負數在前,正數在後;begin爲正數,end爲負數,交換位置
  • 負數在後,正數在前;begin爲負數,end爲正數,交換位置

1、堆、棧


堆是一種經過排序的樹形數據結構,每個結點都有一個值,類似於倒過來的樹結構。
通常我們所說的堆的數據結構,是指二叉堆。堆的特點是根結點的值最小(或最大),且根結點的兩個子樹也是一個堆。由於堆的這個特性,常用來實現優先隊列,堆的存取是隨意。(堆中某個結點的值總是不大於或不小於其父結點的值,堆總是一棵完全二叉樹)

棧就像裝數據的桶或箱子,是一種具有後進先出(FILO)性質的數據結構,也就是說後存放的先取,先存放的後取。
【數據結構的堆和棧和程序結構中所說的堆棧的區別】

1、棧區(stack):由編譯器自動分配釋放,存放函數的參數值,局部變量等值。其操作方式類似於數據結構中的棧
2、堆區(heap):一般由程序員分配釋放,若程序員不釋放,則可能會引起內存泄漏。注堆和數據結構中的堆棧不一樣,其分配方法類是與鏈表。程序結束時可能由OS回收
3、程序代碼區:存放函數體的二進制代碼。
4、數據段:由三部分組成:
1>只讀數據段:
只讀數據段是程序使用的一些不會被更改的數據,使用這些數據的方式類似查表式的操作,由於這些變量不需要更改,因此只需要放置在只讀存儲器中即可。一般是const修飾的變量以及程序中使用的文字常量一般會存放在只讀數據段中。
2>已初始化的讀寫數據段:
已初始化數據是在程序中聲明,並且具有初值的變量,這些變量需要佔用存儲器的空間,在程序執行時它們需要位於可讀寫的內存區域,並且有初值,以供程序運行時讀寫。在程序中一般爲已經初始化的全局變量,已經初始化的靜態局部變量(static修飾的已經初始化的變量)
3>未初始化段(BSS):
未初始化數據是在程序中聲明,但是沒有初始化的變量,這些變量在程序運行之前不需要佔用存儲器的空間。與讀寫數據段類似,它也屬於靜態數據區。但是該段中數據沒有經過初始化。未初始化數據段只有在運行的初始化階段纔會產生,因此它的大小不會影響目標文件的大小。在程序中一般是沒有初始化的全局變量和沒有初始化的靜態局部變量。

2、隊列

定義
隊列是一種先進先出的線性表,隊尾只允許入隊(新增),隊首隻允許出隊(刪除),簡稱FIFO。入隊將一個數據放到隊列尾部;出隊從隊列的頭部取出一個元素。隊列的應用也非常廣泛如:循環隊列、阻塞隊列、併發隊列、優先級隊列等。

隊列的基本操作
EnQueue()——在隊列尾部插入元素
DeQueue()——移除隊列頭部的元素
IsEmpty()——如果隊列爲空,則返回true
Top()——返回隊列的第一個元素
【隊列操作的代碼解釋】:

  1. 入隊的操作是先存入數據、然後將尾指針rear加1,所以尾指針始終指向的是當前的下一個空白區域(未存入數據的地址)
  2. 出隊操作是將當前頭指針front的值清空,然後將頭指針加1
  3. 隊列空的情況——rear == front
  4. 隊列滿的情況——(rear + 1) % maxsize == front
  5. 隊列存數的數量——((rear - front) + maxsize) % maxsize;其中當rear在front的後面時,兩者之差即爲存入的數據量,當rear在front的前面時,兩者之差爲負數,加上maxsize即爲數據量

面試中關於隊列的常見問題
使用隊列表示棧、使用棧來表示隊列
解析:隊列表示棧,需要兩個隊列,一個作爲主隊,壓入數據等,取出棧頂數據等,在出棧操作中,輔助隊列會存入除主隊最後一個元素的其他元素,並將最後一個元素彈出,實現LIFO。
棧表示隊列,需要同樣的兩個棧,壓棧就是隊列的入隊操作,此時第一個元素在棧底,因此出棧時需要將除棧底的其他元素壓入輔助棧,並在主棧彈出元素後,將所有的其他的元素壓回進主棧,實現FIFO。
對隊列的前k個元素倒序、倒置隊列
解析:

反向排列 倒置隊列,就是全部的元素倒序,用遞歸方法;首尾設置標誌從兩邊往裏,互相替換,分爲奇偶兩種情況
倒序排列,順序和排列 每次遍歷隊列,從中找出最小的元素,放入臨時隊列,遍歷的過程是出隊的過程,注意如果一個元素比當前的最小值大,則要放回隊列當中,如果比當前的最小值小,則保存起來,暫時不放回隊列中,發現更小的,把原來的最小值放入,更新最小值,在遍歷完一次以後,將最小值存入臨時隊列。然後開始第二次遍歷,注意每次遍歷原隊列中都會減少一個元素,因此共遍歷隊列N次,每次對隊列N、N-1、N-2 … 1這麼多次出隊操作來找最小值,在最後一次完成後臨時隊列中存放的就是排序好的結果,出隊N次即可按非降序輸出。

使用隊列生成從1到n的二進制數
解析:十進制數化二進制數,整數部分用“除二取餘“法,將數除以2,將餘數放入棧,商再除以2,重複。直至商爲0。小數部分用“乘二取整”法,數乘2,然後取整、放入隊列裏。

3、鏈表

鏈表包括以下類型:
單鏈表(單向)
雙向鏈表(雙向)
雙向鏈表也叫雙鏈表,是鏈表的一種,它的每個數據結點中都有兩個指針,分別指向直接後繼和直接前驅。所以,從雙向鏈表中的任意一個結點開始,都可以很方便地訪問它的前驅結點和後繼結點,一般我們都構造雙向循環鏈表
循環鏈表是鏈表的尾節點指向了前驅中的一個節點,一般指向頭節點
鏈表的基本操作:
InsertAtEnd - 在鏈表的末尾插入指定元素
InsertAtHead - 在鏈接列表的開頭/頭部插入指定元素
Delete - 從鏈接列表中刪除指定元素
DeleteAtHead - 刪除鏈接列表的第一個元素
Search - 從鏈表中返回指定元素
IsEmpty - 如果鏈表爲空,則返回true

進階用法
推薦博文
https://blog.csdn.net/qq9116136/article/details/80056633?depth_1-utm_source=distribute.pc_relevant.none-task&utm_source=distribute.pc_relevant.none-task
0、反向輸出一個鏈表,反向遍歷,輸出顯示
1、刪除一個無頭單鏈表的非尾節點(不能遍歷鏈表)
假設刪除的是pos節點,我們可以將pos的下一個節點的數據存入pos節點,在刪除pos節點的下一個節點即可—將當前的節點的下一個節點的內容存入,有兩份相同的節點內容,可刪除下一個,而非當前的,找不到其前一個節點
2、在無頭單鏈表的一個節點前插入一個節點(不能遍歷鏈表)
假設在pos前插入一個節點,我們可以先在pos後插入一個與pos數據一樣的節點,在將pos的數據改爲需要插入的節點值
3、查找單鏈表的中間節點,(只能遍歷一次鏈表)
給兩個指針,第一個指針走一步時第二個指針走兩步,當快指針走完時慢指針走到中間節點。
查找單鏈表的倒數第K節點,(只能遍歷一次鏈表)
同樣定義兩個指針,快指針先走k步,慢指針開始走,當快指針走完時慢指針走到倒數第K個節點。
4、構造一個帶環鏈表
找到環入口的節點,讓尾節點指向入口節點。
5、求鏈表中環的長度
定義兩個快慢指針,快指針走兩步慢指針走一步,即快指針速度是慢指針的2倍,當他們在環裏第一次相遇時結束,再讓他們繼續走,第二次相遇時快指針一定比慢指針快了環的長度步。

面試中關於鏈表的常見問題
反轉鏈表
遞歸法和迭代法,其中迭代法選取三個指針指向鏈表的1、2、3節點,交換1、2的next內容,互換方向;更新1爲2、2爲3、3---->4,最後判斷2的指向是否爲空。
檢測鏈表中的循環
設定兩個指針p1、p2,每次循環p1向前走一步,p2向前走兩步。直到p2碰到NULL指針或者兩個指針相等結束循環,如果兩個指針相等則說明存在環。
返回鏈表倒數第N個節點
方法以上已解答
刪除鏈表中的重複項
思路一:
遍歷鏈表,把遍歷的值存儲到一個Hashtable中,在遍歷過程中,若當前訪問的值在Hashtable中已經存在,則說明這個數據是重複的,因此就可以刪除。
優點:時間複雜度較低O(n)
缺點:在遍歷過程中需要額外的存儲空間來保存已遍歷過的值
思路二:
對鏈表進行雙重循環遍歷,外循環正常遍歷鏈表,假設外循環當前遍歷的結點爲cur,內循環從cur開始遍歷,若碰到與cur所指向結點值相同,則刪除這個重複結點
優點:不需要額外的存儲空間
缺點:時間複雜度較高O(n^2)
約瑟夫環問題
循環鏈表的使用,數組迭代,遞歸方式

先不斷迭代找到尾節點,讓尾節點的next指針指向頭節點,這樣形成了一個環,
接着進入循環,循環的結束條件爲只剩一個節點。不斷地迭代k次然後刪除當前節點。
		SListNode* str, *tail;
		int i = 0;
		str = pHead;
		tail = pHead;
		while (tail->next)
		{
			tail = tail->next;
		}
		tail->next = pHead;
		while (str->next!=str)
		{
			for (;i < k;++i)
			{
				str = str->next;
			}
			str->data = str->next->data;
			str->next = str->next->next;
			free(str->next);
		}
		return str;

4、二叉樹

常見概念與性質
結點:表示樹中的元素
結點的度:結點含有的子樹數
葉子:度爲0的結點
層次:從根結點算起,根爲第一層
深度:樹中結點最大的層次數
1、二叉樹的第i層至多有2(i-1)個結點
2、深度爲k的二叉樹至多有2k-1個結點
3、對於任何一棵二叉樹T,如果終端結點數爲n0,度爲2的結點數爲n2,則n0 = n2 + 1
定義
二叉樹是每個節點最多有兩棵子樹的樹結構。通常子樹被稱作“左子樹”和“右子樹”,每個節點最多有兩個子樹,即沒有度大於2的節點,且左右順序不能顛倒,二叉樹常被用於實現二叉查找樹和二叉堆,鏈接:https://www.jianshu.com/p/230e6fde9c75
滿二叉樹
一棵深度爲k,且有2k-1個節點的二叉樹稱之爲滿二叉樹,除了葉子結點,所有結點的度爲2
完全二叉樹
深度爲k,有n個節點的二叉樹,當且僅當其每一個節點都與深度爲k的滿二叉樹中,序號爲1至n的節點對應時
①葉子結點只可能出現在層次最大的兩層上
②任意一個結點,其右分支下子孫的最大層次爲I,則其左分支下子孫的最大層次數必爲I或者I+1
二叉排序樹、二叉查找樹、BST
二叉查找樹就是二叉排序樹,也叫二叉搜索樹。二叉查找樹或者是一棵空樹,或者是具有下列性質的二叉樹
(1) 若左子樹不空,則左子樹上所有結點的值均小於它的根結點的值
(2) 若右子樹不空,則右子樹上所有結點的值均大於它的根結點的值
(3) 左、右子樹也分別爲二叉排序樹;(4) 沒有鍵值相等的結點
平衡二叉樹
平衡二叉樹又稱AVL樹,它或者是一棵空樹,或者是具有下列性質的二叉樹:它的左子樹和右子樹都是平衡二叉樹,且左子樹和右子樹的深度之差的絕對值不超過1
AVL樹是最先發明的自平衡二叉查找樹算法。在AVL中任何節點的兩個兒子子樹的高度最大差別爲1,所以它也被稱爲高度平衡樹,n個結點的AVL樹最大深度約1.44log2n。查找、插入和刪除在平均和最壞情況下都是O(log n)。增加和刪除可能需要通過一次或多次樹旋轉來重新平衡這個樹
紅黑樹
紅黑樹是平衡二叉樹的一種,它保證在最壞情況下基本動態集合操作的事件複雜度爲O(log n),紅黑樹和平衡二叉樹區別如下:(1) 紅黑樹放棄了追求完全平衡,追求大致平衡,在與平衡二叉樹的時間複雜度相差不大的情況下,保證每次插入最多隻需要三次旋轉就能達到平衡,實現起來也更爲簡單。(2) 平衡二叉樹追求絕對平衡,條件比較苛刻,實現起來比較麻煩,每次插入新節點之後需要旋轉的次數不能預知
哈夫曼樹
給定n個權值作爲n個葉子結點,構造一棵二叉樹,若該樹的帶權路徑長度達到最小,稱這樣的二叉樹爲最優二叉樹,也稱爲哈夫曼樹(Huffman Tree)。哈夫曼樹是帶權路徑長度最短的樹,權值較大的結點離根較近。
哈夫曼樹和編碼詳細
基本操作
構造二叉樹的方法:
1、順序存儲結構,按照層次結點依次編號,無結點的地方用0替代,浪費空間、適於滿二叉樹和完全二叉樹
2、鏈式存儲結構,data\ lchild\ rchild
3、三叉鏈表,data\ lchild\ rchild\ parent
遍歷的方法:

先序排列 若二叉樹爲空,則空操作,否則先訪問根節點,再先序遍歷左子樹,最後先序遍歷右子樹
中序排列 二叉樹爲空,則空操作,否則先中序遍歷左子樹,再訪問根節點,最後中序遍歷右子樹
後序排列 若二叉樹爲空,則空操作,否則先後序遍歷左子樹,再後序遍歷右子樹,最後訪問根節點
層次排列 若二叉樹爲空,則空操作,否則按照層次結構依次從根結點開始訪問

增刪改查的方法:
刪除結點有三個情況

1.被刪除的結點是葉子結點 直接刪除即可
2.被刪除的結點有一個葉子結點(左右相同) 將其唯一子節點與父結點相連,原來是父結點的左結點,那麼其兒子節點仍連爲父結點的左結點
3.被刪除的結點有兩個葉子結點 右子結點代替被刪除結點,繼承它的位置,原來的左兒子繼續當右兒子的左結點

面試中關於樹結構的常見問題

1、距離根節點距離K的節點 | 距離任意節點距離爲K的節點
2、查找BST中任意2節點的LCA | 查找任意二叉樹中任意2節點的LCA
3、查找給定節點的祖先節點

求二叉樹的高度
【解答】遞歸左右子樹,選擇其中數值大的,即爲樹的高度
在二叉搜索樹中查找第k個最大值
【解答】BST的中序排列時,正好是按照升序的方式,從右往左遍歷即可找到第k個最大值
查找與根節點距離k的節點
【解答】遞歸查詢,直到k次後打印結點內容

  • 若是任意一點的距離爲k的結點,按照向父節點和子孫結點兩個方向搜索,若是距離超過了根節點,則需要按照距離根節點距離x,向根的右結點搜索k-x處結點即可

查找兩個結點的最近公共祖先LCA
【參考解答】
在二叉樹中查找給定節點的祖先節點
【解答】1、順序存儲(完全二叉樹),假設根的存儲下標是1
將當前結點的下標連續整除以2,直到1爲止,中間所有得到的商的下標都是其祖先,並且是從回其雙親直到根爲止
2、鏈式存儲
使用非遞歸的後序遍歷,當遍歷到該結點時,輔助棧中從棧頂到達棧底依次爲該結點從雙親開始到根爲止的所有祖先

非遞歸的後序遍歷二叉樹
do{
while(p != NULL)
{
	st[top++] = p;
	p = p->left;
}
tp = NULL;
flag = 1;
while(top && flag)
{
	p = st[--top];
	if(p->right == tp)
	{
		printf
	}
	else
	{
		p = p->right;
		flag = 0;
	}
}
}
while(top);

【樹類型題目框架】
明確一個節點需要做的事情,剩下的事情交給框架

void tree(TreeNode root)
{
	//root 需要做什麼?
	tree(root.left);
	tree(root.right);
}

5、哈希表

簡單定義
hash函數就是根據key計算出該存儲地址的位置,hash表就是基於hash函數建立的一種查找表。
哈希算法、散列算法
就是把任意長度的輸入(又叫做預映射, pre-image),通過散列算法,變換成固定長度的輸出,該輸出就是散列值。這種轉換是一種壓縮映射,也就是,散列值的空間通常遠小於輸入的空間,不同的輸入可能會散列成相同的輸出,而不可能從散列值來唯一的確定輸入值
哈希表
哈希表是根據設定的哈希函數H(key)和處理衝突方法將一組關鍵字映射到一個有限的地址區間上,並以關鍵字在地址區間中的象作爲記錄在表中的存儲位置,這種表稱爲哈希表或散列,所得存儲位置稱爲哈希地址或散列地址。作爲線性數據結構與表格和隊列等相比,哈希表無疑是查找速度比較快的一種。

key 我們輸入待查找的值
hash函數(散列函數) 存在一種函數F,根據這個函數和查找關鍵字key,可以直接確定查找值所在位置,而不需要一個個遍歷比較。這樣就預先知道key在的位置,直接找到數據,提升效率。即地址index=F(key)
hash值 key通過hash函數算出的值(對數組長度取模,便可得到數組下標)
value 我們想要獲取的內容

運算過程
F(key) = hash
hash % p–(mod m) = index
(該過程需要解決index衝突問題)
Q(index) = value

哈希函數設計考慮因素

計算hash地址的時間 表長
關鍵字的長度 關鍵字是否分佈均勻
儘量減少衝突

設計方法
推薦博文
直接定址法、數字分析法、平方取中法、摺疊法、隨機數法、除留餘數法

解決hash衝突
推薦博文
對不同的關鍵字可能得到同一散列地址,即k1≠k2,而f(k1)=f(k2),或f(k1) MOD 容量 =f(k2) MOD 容量,這種現象稱爲碰撞,亦稱衝突
開放定址法(線性探測再散列、二次探測再散列、僞隨機數散列法)
鏈地址法
再哈希法(建立兩個不同規則的哈希函數)
建立公共溢出區
其中,線性探測再散列比較常用

面試中關於哈希結構的常見問題:
在數組中查找對稱鍵值對
追蹤遍歷的完整路徑
查找數組是否是另一個數組的子集
檢查給定的數組是否不相交

6、圖(有向圖和無向圖)

無向圖和有向圖的建立和遍歷,利用鄰接矩陣和鄰接鏈表
圖的廣度優先搜索和深度優先搜索、BFS、DFS

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章