所謂數(shù)據(jù)結(jié)構(gòu)漠烧,就是數(shù)據(jù)組合在一起并進行管理的方式,這些方式有表靡砌、棧和隊列已脓、樹、圖等通殃。數(shù)據(jù)結(jié)構(gòu)除了儲存數(shù)據(jù)還管理著數(shù)據(jù)度液。
算法呢,就是以某種方式得出所需要的一個數(shù)據(jù)或一組數(shù)據(jù)画舌。實現(xiàn)方式可能是計算堕担,或者遍歷等。
*以上僅為個人理解曲聂,有誤請指出霹购。
一、線性表
概念:線性表是最基本朋腋、最簡單齐疙、也是最常用的一種數(shù)據(jù)結(jié)構(gòu)。其中數(shù)據(jù)元素的排列方式是線性的旭咽。
分類:按照元素儲存結(jié)構(gòu)可分為順序表和鏈式表贞奋。
1.1 順序表
元素的存儲空間是連續(xù)的。比如數(shù)組穷绵。
2.2 鏈表:
元素的存儲空間是離散的轿塔、單獨的,元素之間的邏輯順序是通過鏈表中的指針鏈接次序?qū)崿F(xiàn)仲墨。鏈表也分為很多種勾缭,有單鏈表、雙鏈表宗收、循環(huán)鏈表漫拭,本文暫時分析單鏈表。
單鏈表中的數(shù)據(jù)是以結(jié)點來表示混稽,每個結(jié)點的構(gòu)成:元素(數(shù)據(jù))+指針(指示后繼元素的存儲位置),下面來實現(xiàn)單鏈表审胚。
2.2.1 單鏈表的創(chuàng)建和遍歷
public class LinkList {
// 2.頭結(jié)點和當前結(jié)點
public Node<String> head;
public Node<String> current;
/**
* 3.添加新結(jié)點
* @param data 要添加的結(jié)點的數(shù)據(jù)
*/
public void add(String data){
if(head == null){// 頭結(jié)點為null匈勋,說明鏈表中沒有元素
// 創(chuàng)建結(jié)點并把新結(jié)點賦值給當前結(jié)點
head = new Node<String>(data);
current = head;
} else {
// 創(chuàng)建新節(jié)點賦值給當前結(jié)點的下一個結(jié)點,讓表連起來
current.next = new Node<String>(data);
// 當指針向后移動一位膳叨,保證當前結(jié)點指向最新的結(jié)點
// 這樣再有新數(shù)據(jù)就一直向后鏈接
current = current.next;
}
}
/**
* 4.打印單鏈表中所有結(jié)點數(shù)據(jù)
* @param node 從該結(jié)點開始打印
*/
public void printLinkList(Node node){
if(node == null){
return;
}
current = node;
while (current != null){
System.out.print(current.data);
current = current.next;
}
}
// 1.包含兩部分 data(數(shù)據(jù))洽洁,next(下一個結(jié)點指針)
class Node<E>{
E data;
Node<E> next;
public Node(E data) {
this.data = data;
}
}
}
- 創(chuàng)建結(jié)點。LinkList 表示單鏈表類菲嘴,內(nèi)部類 Node 表示單鏈表的元素饿自。提供構(gòu)造函數(shù)以便創(chuàng)建結(jié)點汰翠。
- 在單鏈表中創(chuàng)建變量頭結(jié)點和當前結(jié)點,頭結(jié)點主要用于記錄首個結(jié)點信息昭雌,當前結(jié)點用于繼續(xù)創(chuàng)建后續(xù)結(jié)點复唤。
- 創(chuàng)建結(jié)點的函數(shù):
如果單鏈表中沒有結(jié)點,則創(chuàng)建結(jié)點傳入數(shù)據(jù)并賦值給頭結(jié)點烛卧。這里頭結(jié)點作為鏈表是否為空的重要屬性佛纫,以及作為遍歷單鏈表所有結(jié)點的起始結(jié)點。
如果不為空鏈表总放,則創(chuàng)建新結(jié)點并賦值給當前的結(jié)點的下一個結(jié)點呈宇,作為連接用。然后將指針向后移動一位指向最新的結(jié)點局雄,這樣當再添加結(jié)點會賦值給最新的結(jié)點的 next 保證鏈接甥啄。 - 單鏈表的遍歷:遍歷的話需要傳遞某個結(jié)點作為參數(shù),并賦值給當前結(jié)點炬搭,如果鏈表存在該結(jié)點則循環(huán)打印結(jié)點數(shù)據(jù)即可型豁。如果想要遍歷所有結(jié)點數(shù)據(jù),就用到變量頭結(jié)點 head 了尚蝌,因為它是作為鏈表的第一個結(jié)點存在的迎变,傳入 head 就可以遍歷所有的結(jié)點了。
測試:
public class Test {
public static void main(String[] args){
LinkList linkList = new LinkList();
// 創(chuàng)建 LinkList 并添加數(shù)據(jù)
// 這時鏈表為空飘言,第一個元素會作為 head 保存衣形。
for (int i = 0; i < 10; i++){
linkList.add("data"+i+"\n");
}
// 這時 head 已經(jīng)有數(shù)據(jù)了,執(zhí)行循環(huán)遍歷即可
linkList.printLinkList(linkList.head);
}
}
結(jié)果:
有關(guān)單鏈表的問題還有很多姿鸿,參考:
相關(guān)問題簡單實現(xiàn):
- 獲取單鏈表長度:遍歷之...
/**
* @return 獲取鏈表長度
*/
public int getLinkListLength(){
if(head == null){
return 0;
}
int length = 0;
Node node = head;
while (node != null){
length++;
node = node.next;
}
return length;
}
- 反轉(zhuǎn)一個單鏈表
/**
* 反轉(zhuǎn)鏈表谆吴,循環(huán)不會棧溢出
* @param node
* @return
*/
public Node reverseList(Node node){
if(node == null || node.next == null){
return node;
}
Node current = node;
Node next = null;
Node reverseHead = null;
while (current != null){
// 1.記錄當前結(jié)點的下一個結(jié)點用于指針后移
next = current.next;
// 4.下一輪循環(huán)時,舊的表的下一結(jié)點鏈接到新表頭
current.next = reverseHead;
// 2.將當前結(jié)點賦值給新表頭
reverseHead = current;
// 3.指針后移
current = next;
}
return reverseHead;
}
這里的思路首先是循環(huán)苛预,然后分別記錄當前結(jié)點句狼、下一個結(jié)點和反轉(zhuǎn)后新鏈表的表頭,接著就是循環(huán)的賦值:
- 記錄當前結(jié)點的下一個結(jié)點热某,用于賦值給 current 實現(xiàn)指針后移腻菇。
- 將當前結(jié)點賦值給新表頭,循環(huán)賦值不斷替換昔馋,這樣保證舊鏈表的最后一個結(jié)點是反轉(zhuǎn)后鏈表的頭結(jié)點筹吐。
- 指針后移,因為現(xiàn)在的 next 已經(jīng)是剛才 current 的下一結(jié)點了秘遏。
- 下一輪循環(huán)時丘薛,舊的表的下一結(jié)點鏈接到新表頭:因為第一輪循環(huán) reverseHead 是 null,所以不用理會邦危。到第二個循環(huán)時洋侨,這時的 reverseHead 經(jīng)過第 1 步已經(jīng)有值且值是初始結(jié)點舍扰,將 current.next 也就是第三個結(jié)點指向初始結(jié)點,然后再循環(huán)處理到最后實現(xiàn)反轉(zhuǎn)希坚。
-
查找單鏈表中間結(jié)點
一種思路是遍歷獲取鏈表長度再拿中間边苹,這里記錄不允許遍歷的情況:
/**
* 查找單鏈表中間
* @param head
* @return
*/
public Node findMidNode(Node head){
if(head == null){
return null;
}
// 定義兩個
Node first = head;
Node second = head;
// 兩個同時移動,但是一個走一步另一個走兩步
while (second != null && second.next != null){
first = first.next;
second = second.next.next;
}
return first;
}
思路是定義兩個結(jié)點吏够,起點一樣勾给,循環(huán)一個走一步一個走兩步,這樣當走的最快的走不下去時第一個就是中間結(jié)點了锅知。
-
合并兩個有序鏈表播急,合并后依然有序
例如:
鏈表1:1 > 2 > 3 > 4
鏈表2:2 > 3 > 4 > 5
合并后:1 > 2 > 2 > 3 > 3 > 4 > 4 > 5
/**
* 合并兩個有序鏈表
* @param head1 鏈表1 表頭
* @param head2 鏈表2 表頭
* @return
*/
public Node mergeList(Node head1,Node head2){
// 如果兩個鏈表都為 null 返回 null
if(head1 == null && head2 == null){
return null;
}
// 如果其中一個為 null,返回不為 null 的就是要的表頭了
if(head1 == null){
return head2;
}
if(head2 == null){
return head1;
}
Node head = null;
Node current = null;
// 判斷首位售睹,確認新表頭
if((int)head1.data < (int)head2.data ){
head = head1;
current = head1;
head1 = head1.next;
} else {
head = head2;
current = head2;
head2 = head2.next;
}
while (head1 != null && head2 != null){
if((int)head1.data < (int)head2.data){
current.next = head1; // 循環(huán)比較桩警,把較小的值鏈接到新表后
current = current.next;// 指針后移
head1 = head1.next;
} else {
current.next = head2;
current = current.next;
head2 = head2.next;
}
}
// 經(jīng)過上面循環(huán),head1 還有值說明 head2 循環(huán)完畢
if(head1 != null){
// 后面是有序的昌妹,所以只要鏈接到當前結(jié)點即可
current.next = head1;
}
// 同理捶枢,head1 循環(huán)完畢
if(head2 != null){
current.next = head2;
}
return head;
}
自行理會吧...233333.
二、棧
特點:先進后出(后進先出)飞崖。也就是說烂叔,保存在棧中的元素會像摞書本一樣一個一個壓在棧中,取出來的時候會一本一本拿固歪。
Java Stack 類:
-
push()
: 往棧中添加數(shù)據(jù) -
pop()
: 返回棧頂?shù)闹挡h除 -
peek()
: 返回棧頂?shù)闹邓饧Γ粍h除 -
search(Object o)
: 返回元素 o 最后出現(xiàn)下標
一個簡單的基于數(shù)組的實現(xiàn):
public class Stack {
// 記錄棧頂坐標,同時也可依據(jù)該值獲取棧頂元素
private int top = -1;
private Object[] data;
public Stack(int capacity) throws Exception {
if(capacity < 0){
throw new Exception("Illegal capacity:"+capacity);
}
data = new Object[capacity];
}
public Object push(Object o) throws Exception {
if(top == data.length - 1){
throw new Exception("Stack is full!");
}
return data[++top] = o;
}
public Object pop() throws Exception {
if(top == -1){
throw new Exception("Stack is empty!");
}
return data[top--];
}
public void print(){
System.out.print("bottom -> top: | ");
for(int i = 0 ; i <= top ; i++){
System.out.print(data[i]+" | ");
}
System.out.print("\n");
}
}
一個簡單的基于鏈表的實現(xiàn):
public class LinkedStack {
private LinkedList linkedList = new LinkedList();
public void push(Object o){
linkedList.addFirst(o);
}
public Object pop(){
return linkedList.removeFirst();
}
}
三牢裳、隊列
特點:先進先出 即最先放入的元素最先被取出逢防。元素存放時類似棧,按順序排列蒲讯。元素存儲時從隊尾放入忘朝,取出時則先從隊頭出隊。
隊列的創(chuàng)建:
一般情況下判帮,隊列創(chuàng)建的方式有兩種:
- 基于數(shù)組創(chuàng)建(順序隊列)
- 基于鏈表創(chuàng)建(鏈式隊列)
一個簡單的隊列的實現(xiàn):
public class Queue {
public Node head;
public Node current;
/**
* 與鏈表添加數(shù)據(jù)方式類似
* @param data 數(shù)據(jù)
*/
public void addNode(int data){
if(head == null){
head = new Node(data);
current = head;
} else {
current = new Node(data);
current = current.next;
}
}
/**
* 出隊列永遠都是隊頭的數(shù)據(jù)
* @return head 的數(shù)據(jù)
* @throws Exception
*/
public int pop() throws Exception {
if(head == null){
throw new Exception("隊列為空");
}
Node node = head; //要出隊的Node
head = node.next; //指針后移鸭轮,因為頭已經(jīng)出隊
return node.data;
}
class Node{
Node next;
int data;
public Node(int data){
this.data = data;
}
}
}
四汇跨、樹(多數(shù)資料來自百度百科)
??樹狀圖是一種數(shù)據(jù)結(jié)構(gòu)谆刨,它是由n(n>=1)個有限節(jié)點組成一個具有層次關(guān)系的集合鉴逞。把它叫做“樹”是因為它看起來像一棵倒掛的樹,也就是說它是根朝上偎痛,而葉朝下的。它具有以下的特點:
??每個節(jié)點有零個或多個子節(jié)點独郎;沒有父節(jié)點的節(jié)點稱為根節(jié)點踩麦;每一個非根節(jié)點有且只有一個父節(jié)點枚赡;除了根節(jié)點外,每個子節(jié)點可以分為多個不相交的子樹谓谦;
定義:
樹(tree)是包含n(n>0)個結(jié)點的有窮集贫橙,其中:
(1) 每個元素稱為結(jié)點(node);
(2) 有一個特定的結(jié)點被稱為根結(jié)點或樹根(root)反粥。
(3) 除根結(jié)點之外的其余數(shù)據(jù)元素被分為m(m≥0)個互不相交的集合T1卢肃,T2,……Tm-1才顿,其中每一個集合Ti(1<=i<=m)本身也是一棵樹莫湘,被稱作原樹的子樹(subtree)。
相關(guān)概念:
- 樹的高度/深度郑气,結(jié)點的層次(Level):從根開始定義起幅垮,根為第一層,根的子結(jié)點為第二層尾组。依次類推忙芒,如上圖的樹深度/高度為 4。
- 森林:m(m>=0)棵互不相交的樹的集合讳侨。
種類:
- 無序樹:樹中任意節(jié)點的子結(jié)點之間沒有順序關(guān)系呵萨,可以互換,這種樹稱為無序樹,也稱為自由樹;
- 有序樹:樹中任意節(jié)點的子結(jié)點之間有順序關(guān)系跨跨,這種樹稱為有序樹潮峦;
- 二叉樹:每個節(jié)點最多含有兩個子樹的樹稱為二叉樹;
- 完全二叉樹:若設(shè)二叉樹的深度為h歹叮,除第 h 層外跑杭,其它各層 (1~h-1) 的結(jié)點數(shù)都達到最大個數(shù),第 h 層所有的結(jié)點都連續(xù)集中在最左邊咆耿,這就是完全二叉樹德谅。
- 滿二叉樹:除最后一層無任何子節(jié)點外,每一層上的所有結(jié)點都有兩個子結(jié)點二叉樹萨螺。也就是說窄做,如果一個二叉樹的層數(shù)為K,且結(jié)點總數(shù)是(2^k) -1 慰技,則它就是滿二叉樹椭盏。
- 哈夫曼樹:給定n個權(quán)值作為n個葉子結(jié)點,構(gòu)造一棵二叉樹吻商,若帶權(quán)路徑長度達到最小掏颊,稱這樣的二叉樹為最優(yōu)二叉樹,也稱為哈夫曼樹(Huffman Tree)。哈夫曼樹是帶權(quán)路徑長度最短的樹乌叶,權(quán)值較大的結(jié)點離根較近盆偿。
二叉樹
二叉樹的結(jié)點:
class TreeNode<E>{
E data;
TreeNode left;
TreeNode right;
public TreeNode(E data){
this.data = data;
}
}
二叉樹的創(chuàng)建:
private List<TreeNode> datas;//存儲所有的節(jié)點
public TreeNode root;//根節(jié)點
public BinaryTree(){
}
public void createTree(Object[] objs){
datas = new ArrayList<>();
for (Object object : objs) {
datas.add(new TreeNode(object));
}
root=datas.get(0);//將第一個元素作為根節(jié)點
for(int i = 0; i < objs.length/2; i++){
datas.get(i).left = datas.get(i*2+1);
if(i*2+2 < datas.size()){// 避免下標越界
datas.get(i).right = datas.get(i*2+2);
}
}
}
二叉樹的遍歷:
二叉樹的遍歷方式有很多,如果限制了從左到右的方式准浴,主要可以分為四種:
-
前序/先序遍歷
先訪問根結(jié)點事扭,然后前序遍歷左子樹,再前序遍歷右子樹乐横。
//前序/先序遍歷
public void preorder(TreeNode<E> root){
if(root == null){
return;
}
System.out.print(root.data+"");
preorder(root.left);
preorder(root.right);
}
- 中序遍歷
//中序遍歷
public void inorder(TreeNode<E> root){
if(root == null){
return;
}
inorder(root.left);
System.out.print(root.data+"");
inorder(root.right);
}
- 后序遍歷
//后序遍歷
public void postorder(TreeNode<E> root) {
if (root == null){
return;
}
postorder(root.left);
postorder(root.right);
System.out.println(root.data + " ");
}
三種遍歷方式都用到了遞歸求橄,從代碼上看區(qū)別就是打印數(shù)據(jù)的位置。
二叉樹的高度:
同樣是利用遞歸以及左右子結(jié)點判斷逐漸增加高度葡公。
public int height(TreeNode root) {
if (root == null)
return 0;// 遞歸結(jié)束:空樹高度為0
else {
int i = height(root.left);
int j = height(root.right);
return (i < j) ? (j + 1) : (i + 1);
}
}
二叉樹的結(jié)點數(shù)量:
public int size(TreeNode root) {
if (root == null) {
return 0;
} else {
return 1 + size(root.left) + size(root.right);
}
}
二叉樹的結(jié)點插入:
public String insert(int value) {
String error = null;
TreeNode node = new TreeNode(value);
if (root == null) {
root = node;
root.left = null;
root.right = null;
} else {
TreeNode current = root;
TreeNode parent = null;
while (true) {
if (value < (int)current.data) {
parent = current;
current = current.left;
if (current == null) {
parent.left = node;
break;
}
} else if (value > (int)current.data) {
parent = current;
current = current.right;
if (current == null) {
parent.right = node;
break;
}
} else {
error = "having same value in binary tree";
System.out.print(error);
}
} // end of while
}
return error;
}
二叉樹的結(jié)點刪除:
/**
* 刪除結(jié)點
* @param value
* @return
*/
public boolean delete(int value) {
TreeNode current = root; //需要刪除的節(jié)點
TreeNode parent = null; //需要刪除的節(jié)點的父節(jié)點
boolean isLeftChild = true; //需要刪除的節(jié)點是否父節(jié)點的左子樹
while (true) {
if (value == (int)current.data) {
break;
} else if (value < (int)current.data) {
isLeftChild = true;
parent = current;
current = current.left;
} else {
isLeftChild = false;
parent = current;
current = current.right;
}
//找不到需要刪除的節(jié)點罐农,直接返回
if (current == null)
return false;
}
//分情況考慮
//1、需要刪除的節(jié)點為葉子節(jié)點
if (current.left == null && current.right == null) {
//如果該葉節(jié)點為根節(jié)點匾南,將根節(jié)點置為null
if (current == root) {
root = null;
} else {
//如果該葉節(jié)點是父節(jié)點的左子節(jié)點啃匿,將父節(jié)點的左子節(jié)點置為null
if (isLeftChild) {
parent.left = null;
} else { //如果該葉節(jié)點是父節(jié)點的右子節(jié)點,將父節(jié)點的右子節(jié)點置為null
parent.right = null;
}
}
}
//2蛆楞、需要刪除的節(jié)點有一個子節(jié)點溯乒,且該子節(jié)點為左子節(jié)點
else if (current.right == null) {
//如果該節(jié)點為根節(jié)點,將根節(jié)點的左子節(jié)點變?yōu)楦?jié)點
if (current == root) {
root = current.left;
} else {
//如果該節(jié)點是父節(jié)點的左子節(jié)點豹爹,將該節(jié)點的左子節(jié)點變?yōu)楦腹?jié)點的左子節(jié)點
if (isLeftChild) {
parent.left = current.left;
} else { //如果該節(jié)點是父節(jié)點的右子節(jié)點裆悄,將該節(jié)點的左子節(jié)點變?yōu)楦腹?jié)點的右子節(jié)點
parent.right = current.left;
}
}
}
//2、需要刪除的節(jié)點有一個子節(jié)點臂聋,且該子節(jié)點為右子節(jié)點
else if (current.left == null) {
//如果該節(jié)點為根節(jié)點光稼,將根節(jié)點的右子節(jié)點變?yōu)楦?jié)點
if (current == root) {
root = current.right;
} else {
//如果該節(jié)點是父節(jié)點的左子節(jié)點,將該節(jié)點的右子節(jié)點變?yōu)楦腹?jié)點的左子節(jié)點
if (isLeftChild) {
parent.left = current.right;
} else { //如果該節(jié)點是父節(jié)點的右子節(jié)點孩等,將該節(jié)點的右子節(jié)點變?yōu)楦腹?jié)點的右子節(jié)點
parent.right = current.right;
}
}
}
//3艾君、需要刪除的節(jié)點有兩個子節(jié)點,需要尋找該節(jié)點的后續(xù)節(jié)點替代刪除節(jié)點
else {
TreeNode successor = getSuccessor(current);
//如果該節(jié)點為根節(jié)點肄方,將后繼節(jié)點變?yōu)楦?jié)點冰垄,并將根節(jié)點的左子節(jié)點變?yōu)楹罄^節(jié)點的左子節(jié)點
if (current == root) {
root = successor;
} else {
//如果該節(jié)點是父節(jié)點的左子節(jié)點,將該節(jié)點的后繼節(jié)點變?yōu)楦腹?jié)點的左子節(jié)點
if (isLeftChild) {
parent.left = successor;
} else { //如果該節(jié)點是父節(jié)點的右子節(jié)點权她,將該節(jié)點的后繼節(jié)點變?yōu)楦腹?jié)點的右子節(jié)點
parent.right = successor;
}
}
}
current = null;
return true;
}
/**
*
* 得到后繼節(jié)點虹茶,即刪除節(jié)點的左后代
*/
private TreeNode getSuccessor(TreeNode delNode) {
TreeNode successor = delNode;
TreeNode successorParent = null;
TreeNode current = delNode.right;
while (current != null) {
successorParent = successor;
successor = current;
current = current.left;
}
//如果后繼節(jié)點不是刪除節(jié)點的右子節(jié)點時,
if (successor != delNode.right) {
//要將后繼節(jié)點的右子節(jié)點指向后繼結(jié)點父節(jié)點的左子節(jié)點隅要,
successorParent.left = successor.right;
//并將刪除節(jié)點的右子節(jié)點指向后繼結(jié)點的右子節(jié)點
successor.right = delNode.right;
}
//任何情況下蝴罪,都需要將刪除節(jié)點的左子節(jié)點指向后繼節(jié)點的左子節(jié)點
successor.left = delNode.left;
return successor;
}
測試:
public static void main(String[] args){
BinaryTree binTree=new BinaryTree();
Object[] objs={0,1,2,3,4,5,6,7,8,9};
binTree.createTree(objs);
binTree.insert(10);
binTree.preorder(binTree.root);
// binTree.inorder(binTree.root);
// binTree.postorder(binTree.root);
// System.out.print(binTree.height(binTree.root)+" ");
}
關(guān)于二叉樹更多資料:
五、圖
暫時還沒有用到圖步清,等到用到的時候再來復習要门。
六、排序
有空記得自己實現(xiàn)。
其它比較好的文章:
Android程序員面試會遇到的算法(part 1 關(guān)于二叉樹的那點事) 附Offer情況
那些年询微,我們被問到的數(shù)據(jù)結(jié)構(gòu)