206. 反轉連結串列-力扣(leetcode)
文章目錄
一、二叉樹概述
1.1 為什麼要有樹這種資料結構
在前面已經學習了陣列和連結串列這兩種資料結構,這兩種資料結構都有著鮮明的特點。
陣列儲存方式的優缺點如下:
- 優點:通過下標方式訪問元素,速度快。對於有序陣列,還可使用二分查詢提高檢索速度。
- 缺點:如果要檢索具體某個值,或者插入值(按一定順序)會整體移動, 效率較低。
連結串列儲存方式的優缺點如下:
- 優點:在一定程度上對陣列儲存方式有優化。比如插入一個數值節點,只需要將插入節點連結到連結串列中即可,其刪除效率也較高。
- 缺點:在進行檢索時,效率仍然較低。比如檢索某個值, 需要從頭節點開始遍歷。
從它們的特點我們可以總結出:陣列方式查詢快、增刪慢,而連結串列方式查詢慢、增刪快。這兩種資料儲存方式都相當於是魚與熊掌不可兼得。
而樹這種資料結構,可以在查詢速度快的同時又兼顧了增刪的效率,可以說是結合了陣列和連結串列的優點。
樹儲存方式的核心特點為:能提高資料儲存、讀取的效率。比如利用二叉排序樹(Binary Sort Tree),既可以保證資料的檢索速度,同時也可以保證資料的插入、刪除、修改的速度。
1.2 樹的常用術語
樹這種資料結構有較多的常用術語,這些術語我們需要熟練記住。
這些常用術語包括:
- 節點
- 根節點
- 父節點
- 子節點
- 葉子節點(沒有子節點的節點)
- 節點的權(節點值)
- 路徑(從 root 節點找到該節點的路線)
- 層
- 子樹
- 樹的高度(最大層數)
- 森林(多顆子樹構成森林)
常用術語雖然比較多,但是對照著樹的示意圖,還是比較容易理解的。
1.3 什麼是二叉樹
樹有很多種,每個節點最多隻能有兩個子節點的一種形式稱為二叉樹。二叉樹的子節點分為左節點和右節點。
如上圖所示的三個樹,它們每個節點最多隻有兩個子節點,因此它們都是二叉樹。
在二叉樹中,還有兩種更為特殊的樹,分別是:
-
滿二叉樹
如果二叉樹的所有葉子節點都在最後一層
-
完全二叉樹
如果二叉樹的所有葉子節點都在最後一層或者倒數第二層,而且最後一層的葉子節點在左邊(相對於根節點)連續,倒數第二層的葉子節點在右邊連續,我們稱為完全二叉樹。
我們不難看出:滿二叉樹是完全二叉樹的特殊形態, 即如果一棵二叉樹是滿二叉樹, 則它必定是完全二叉樹。
1.4 二叉樹的遍歷順序
二叉樹的遍歷共有三種情況:
-
前序遍歷
先遍歷根節點,再遍歷左子樹,最後遍歷右子樹。
-
中序遍歷
先遍歷左子樹,再遍歷根節點,最後遍歷右子樹。
-
後序遍歷
先遍歷左子樹,再遍歷右子樹,最後遍歷根節點。
根據這三種遍歷順序的定義,我們可以得知兩個結論:
- 左子樹總是在右子樹的前面遍歷;
- 前、中、後的遍歷順序指的是遍歷根節點的順序。
二、二叉樹的基本操作
本節的關於二叉樹的操作,都是對如下圖所示的二叉樹進行的:
如上所示的二叉樹,每一個節點都是一個英雄節點 HeroNode
,每個節點記錄著英雄的資訊(編號、姓名)。
英雄節點模型如下:
/**
* 模擬二叉樹的節點
*/
class HeroNode{
public int no;
public String name;
public HeroNode left; // 左子節點
public HeroNode right; // 右子節點
public HeroNode(int no, String name){
this.no = no;
this.name = name;
}
// 新增左子節點
public void addLeftNode(HeroNode node){
this.left = node;
}
// 新增右子節點
public void addRightNode(HeroNode node){
this.right = node;
}
// ......
@Override
public String toString() {
return "HeroNode{" +
"no=" + no +
", name='" + name + '\'' +
'}';
}
}
除此之外,我們還需要建立一個二叉樹模型 BinaryTree
,包含對二叉樹的操作:
class BinaryTree{
private HeroNode root; // 根節點
public BinaryTree(HeroNode node){
this.root = node;
}
// ......
}
為了方便後續的程式碼測試,我們手動建立一個上圖所示的二叉樹:
HeroNode node_1 = new HeroNode(1, "宋江");
HeroNode node_2 = new HeroNode(2, "盧俊義");
HeroNode node_3 = new HeroNode(3, "吳用");
HeroNode node_4 = new HeroNode(4, "公孫勝");
HeroNode node_5 = new HeroNode(5, "關勝");
BinaryTree binaryTree = new BinaryTree(node_1);
node_1.addLeftNode(node_2);
node_1.addRightNode(node_3);
node_3.addLeftNode(node_4);
node_3.addRightNode(node_5);
2.1 二叉樹的節點遍歷
【案例需求】
使用前序、中序、後序分別遍歷二叉樹。
【思路分析】
上面說過,二叉樹的遍歷包括:前序遍歷、中序遍歷、後序遍歷。
其中前序遍歷的思路如下:
- 首先輸出當前節點;
- 如果左子節點不為空,就對左子節點遞迴前序遍歷;
- 如果右子節點不為空,就對右子節點遞迴前序遍歷。
中序遍歷的思路如下:
- 首先判斷左子節點是否為空,如果不為空,就對左子節點遞迴中序遍歷;
- 然後輸出當前節點;
- 最後判斷右子節點是否為空,如果不為空,就對右子節點遞迴中序遍歷。
後序遍歷的思路如下:
- 首先判斷左子節點是否為空,如果不為空,就對左子節點遞迴後序遍歷;
- 然後判斷右子節點是否為空,如果不為空,就對右子節點遞迴後序遍歷;
- 最後輸出當前節點。
【程式碼實現】
英雄節點 HeroNode
負責對子樹的具體操作,二叉樹 BinaryTree
呼叫節點的具體操作來實現相關操作。
二叉樹的遍歷操作實現程式碼如下:
/**
* 二叉樹
*/
class BinaryTree{
private HeroNode root; // 根節點
public BinaryTree(HeroNode node){
this.root = node;
}
// 前序遍歷
public void preOrder(){
if (root != null){
root.preOrder(); // 根節點呼叫具體遍歷操作
}else{
System.out.println("二叉樹為空!");
}
}
// 中序遍歷
public void midOrder(){
if (root != null){
root.midOrder(); // 根節點呼叫具體遍歷操作
}else{
System.out.println("二叉樹為空!");
}
}
// 後序遍歷
public void postOrder(){
if (root != null){
root.postOrder(); // 根節點呼叫具體遍歷操作
}else{
System.out.println("二叉樹為空!");
}
}
}
/**
* 模擬二叉樹的節點
*/
class HeroNode{
public int no;
public String name;
public HeroNode left; // 左子節點
public HeroNode right; // 右子節點
public HeroNode(int no, String name){
this.no = no;
this.name = name;
}
// 新增左子節點
public void addLeftNode(HeroNode node){
this.left = node;
}
// 新增右子節點
public void addRightNode(HeroNode node){
this.right = node;
}
// 前序遍歷:根->左->右
public void preOrder(){
System.out.println(this); // 列印本節點
if (this.left != null){ // 遞迴列印左子節點
this.left.preOrder();
}
if (this.right != null){ // 遞迴列印右子節點
this.right.preOrder();
}
}
// 中序遍歷:左->根->右
public void midOrder(){
if (this.left != null){ // 遞迴列印左子節點
this.left.midOrder();
}
System.out.println(this); // 列印本節點
if (this.right != null){
this.right.midOrder(); // 遞迴列印右子節點
}
}
// 後序遍歷:左->右->根
public void postOrder(){
if (this.left != null){ // 遞迴列印左子節點
this.left.postOrder();
}
if (this.right != null){ // 遞迴列印右子節點
this.right.postOrder();
}
System.out.println(this); // 列印本節點
}
@Override
public String toString() {
return "HeroNode{" +
"no=" + no +
", name='" + name + '\'' +
'}';
}
}
程式碼測試執行結果如下:
2.2 二叉樹的節點查詢
【案例需求】
分別使用前序、中序、後序查詢二叉樹中指定編號節點。
【思路分析】
前序查詢思路分析:
- 首先判斷當前節點的編號是否等於指定編號
- 如果相等,則直接返回當前節點;
- 如果不相等,再判斷左子節點是否為空,如果不為空則左子節點遞迴前序查詢;
- 如果左子節點遞迴前序查詢找到了節點,則返回節點;
- 否則,繼續判斷右子節點是否為空,如果不為空則對右子節點遞迴前序查詢;
- 如果找到則返回節點,否則返回 NULL。
中序查詢思路:
- 首先判斷判斷左子節點是否為空,如果不為空則左子節點遞迴中序查詢;
- 如果左子節點遞迴中序查詢找到了節點,則返回節點;
- 否則,判斷當前節點的編號是否等於指定編號,如果相等,則直接返回當前節點;
- 否則,繼續判斷右子節點是否為空,如果不為空則對右子節點遞迴中序查詢;
- 如果找到則返回節點,否則返回 NULL。
後序查詢思路:
- 首先判斷判斷左子節點是否為空,如果不為空則左子節點遞迴後序查詢;
- 如果左子節點遞迴後序查詢找到了節點,則返回節點;
- 否則,繼續判斷右子節點是否為空,如果不為空則對右子節點遞迴後序查詢;
- 如果右子節點遞迴後序查詢找到了節點,則返回節點;
- 否則,判斷當前節點的編號是否等於指定編號。如果相等,則直接返回當前節點,否則,返回 NULL。
【程式碼實現】
前、中、後序查詢指定編號的節點的程式碼實現如下:
/**
* 二叉樹
*/
class BinaryTree{
private HeroNode root; // 根節點
public BinaryTree(HeroNode node){
this.root = node;
}
// 前序查詢
public void preOrderSearch(int no){
if (root != null){ // 判斷根節點是否為空
HeroNode res = root.preOrderSearch(no);
if (res != null){
System.out.println(res);
}else{
System.out.println("未找到指定節點!");
}
}else{
System.out.println("二叉樹為空!");
}
}
// 中序查詢
public void midOrderSearch(int no){
if (root != null){
HeroNode res = root.midOrderSearch(no);
if (res != null){
System.out.println(res);
}else{
System.out.println("未找到指定節點!");
}
}else{
System.out.println("二叉樹為空!");
}
}
// 後序查詢
public void postOrderSearch(int no){
if (root != null){
HeroNode res = root.postOrderSearch(no);
if (res != null){
System.out.println(res);
}else{
System.out.println("未找到指定節點!");
}
}else{
System.out.println("二叉樹為空!");
}
}
}
/**
* 模擬二叉樹的節點
*/
class HeroNode{
public int no;
public String name;
public HeroNode left; // 左子節點
public HeroNode right; // 右子節點
public HeroNode(int no, String name){
this.no = no;
this.name = name;
}
// 新增左子節點
public void addLeftNode(HeroNode node){
this.left = node;
}
// 新增右子節點
public void addRightNode(HeroNode node){
this.right = node;
}
// 前序查詢:根左右
public HeroNode preOrderSearch(int no){
if (this.no == no){ // 先查根
return this;
}
HeroNode res = null;
if (this.left != null){ // 再查左
res = this.left.preOrderSearch(no);
if (res != null){
return res;
}
}
if (this.right != null){ // 最後查右
res = this.right.preOrderSearch(no);
}
return res;
}
// 中序查詢:左根右
public HeroNode midOrderSearch(int no){
HeroNode res = null;
if (this.left != null){ // 先查左
res = this.left.midOrderSearch(no);
}
if (res != null){
return res;
}
if (this.no == no){ // 再查根
return this;
}
if (this.right != null){ // 最後查右
res = this.right.midOrderSearch(no);
}
return res;
}
// 後序查詢:左右根
public HeroNode postOrderSearch(int no){
HeroNode res = null;
if (this.left != null){ // 先左子節點
res = this.left.postOrderSearch(no);
}
if (res != null){
return res;
}
if (this.right != null){ // 然後右子節點
res = this.right.postOrderSearch(no);
}
if (res != null){
return res;
}
if (this.no == no){ // 最後根節點
return this;
}
return null;
}
@Override
public String toString() {
return "HeroNode{" +
"no=" + no +
", name='" + name + '\'' +
'}';
}
}
2.3 二叉樹的節點刪除
【案例需求】
根據節點編號刪除指定節點:
- 如果節點為葉子節點,直接刪除;
- 如果節點為父節點,則把節點和節點的整個子樹都刪除。
【思路分析】
除了二叉樹的根節點之外,其餘的節點都是有父節點的。由於二叉樹的每個節點關係都是單向的,即每個節點記錄的都是自己的左、右子節點的資訊,所以是無法通過目標節點自己來刪除自己的,而是需要藉助目標節點的父節點來刪除目標節點。
因此二叉樹的節點刪除思路如下:
- 如果二叉樹的根節點就是目標節點,那麼直接將根節點置空即可;
- 否則,如果當前根節點的左子節點不為空且是目標節點,就將左子節點置空
this.left = null
並返回; - 如果第 2 步沒刪除,若當前根節點的右子節點不為空且是目標節點,就將右子節點置空
this.right = null
並返回; - 如果第 3 步沒刪除,就遍歷左子樹遞迴查詢刪除;
- 如果第 4 步也沒刪除,就遍歷右子樹遞迴查詢刪除;
【程式碼實現】
刪除二叉樹指定節點的程式碼如下:
class BinaryTree{
private HeroNode root; // 根節點
public BinaryTree(HeroNode node){
this.root = node;
}
// 刪除指定編號的節點
public void deleteNode(int no){
if (root != null){ // 判斷根節點是否為空
if (root.no == no){ // 判斷根節點是否為目標節點
root = null;
}else{
root.deleteNode(no); // 從根節點開始遞迴遍歷子節點
}
}else{
System.out.println("二叉樹為空");
}
}
}
/**
* 模擬二叉樹的節點
*/
class HeroNode{
public int no;
public String name;
public HeroNode left; // 左子節點
public HeroNode right; // 右子節點
public HeroNode(int no, String name){
this.no = no;
this.name = name;
}
// 新增左子節點
public void addLeftNode(HeroNode node){
this.left = node;
}
// 新增右子節點
public void addRightNode(HeroNode node){
this.right = node;
}
// 刪除節點:由於節點只記錄下自己的左、右子節點的資訊,因為只能通過待刪除節點的父節點來刪除自己
public void deleteNode(int no){
if (this.left != null && this.left.no == no){ // 判斷左子節點是否是目標節點
this.left = null; // 刪除節點,如果節點為父節點,則子樹全部刪除
return;
}
if (this.right != null && this.right.no == no){ // 判斷右子節點是否為目標節點
this.right = null; // 刪除節點,若節點為父節點,則子樹全部刪除
return;
}
if (this.left != null){ // 如果左子樹不為空,左子樹遞迴執行
this.left.deleteNode(no);
}
if (this.right != null){ // 如果右子樹不為空,右子樹遞迴執行
this.right.deleteNode(no);
}
}
@Override
public String toString() {
return "HeroNode{" +
"no=" + no +
", name='" + name + '\'' +
'}';
}
}