Unity游戏开发基础之数据结构部分

news2024/12/29 9:15:42

设计模式

含义:帮助我们降低对象之间的耦合度常用的方法称为设计模式。使用设计模式是为了可重用代码,让代码更容易被其他人所理解,保证代码可靠性,使代码编制真正工程化,这是软件工程的基石。
分类:
创建型模式:工厂方法模式、抽象工厂模式、单例模式、建造者模式、组合模式、原型模式。
结构型模式:适配器模式、装饰器模式、代理模式、外观模式、桥接模式、组合模式、享元模式。
行为型模式:策略模式、模板方法模式、观察者模式、迭代子模式、责任链模式、命令模式、备忘录模式、状态模式、访问者模式、中介者模式、解释器模式。
重点:单例模式、工厂方法模式、创造者模式、适配器模式、代理模式、装饰器模式、策略模式、责任链模式、观察者模式、迭代器模式。
超重点:单例模式,有饿汉式,懒汉式、双检锁、静态内部类、线程单例、枚举。

单例模式

含义:一个类只有一个实例,只在内部实例一次,外部无法实例化,全局唯一。一般应用于各种管理器,如角色管理器、声音管理器,场景管理器。

public class MySingleton
{
  private static MySingleton __instance;
  public static MySingleton instance{
    get{
      if(__instance == null){
        __instance = new MySIngleton();
      }
    }
  }
  public MySingleton(){
    Debug.Log("执行构造函数");
  }
  public void Show(){
    Debug.Log("show");
  }
}
MySingleton.instance.Show();

观察者模式

含义:可称为发布/订阅模式,定义了一种一对多的依赖关系,让多个观察者对象同时监听某一个主题对象(多播委托)。主题对象发生变化时,它们能够自动更新自己,通常用作实现事件处理系统。

Using System;
using UnityEngine;
public class Animal{
  protected string Name;
  public Animal(string name){
    this.Name = name;
  }
  public virtual void Run(){

  }
}
public class Cat:Animal{
  public Action actions;  //发布者
  public Cat(string name):base(name){

  }
  public void Coming(/*MouseA mouseA,MouseB mouseB,MouseC mouseC*/){
    Debug.Log(Name + "猫来了");
    //mouseA.Run();
    //mouseB.Run();
    //mouseC.Run();
    this.Run();
    if(actions!= null){
      actions();
    }
  }
  public override void Run(){
    Debug.Log(Name + "追老鼠");
  }
}


public class Mouse:Animal{
  public Mouse(string name,Cat cat):base(name){
    cat.actions += this.Run;  //订阅者
  }
  public override void Run(){
    Debug.Log(Name + "老鼠跑");
  }
}



public class Visit:monoBehaviour{
  void Start(){
    Cat cat = New Cat("小野猫");
    Mouse mouseA = new Mouse("mouseA",cat);
    Mouse mouseB = new Mouse("mouseB",cat);
    Mouse mouseC = new Mouse("mouseC",cat);
    // 在老鼠数量不确定的情况下,这样的设计是不合理的
    //cat.Coming(mouseA,mouseB,mouseC);
    // 这样合理
    cat.Coming();
  }
}

简单工厂/工厂/抽象工厂

简单工厂模式:又叫静态工厂方法模式,由一个工厂对象决定创建出哪一种产品类的实例。只生产一个品类的产品,在工厂中动态创建。
工厂模式:避免简单工厂模式中,新增产品品牌(类型)时,直接修改工厂类。通过多个工厂创建不同的产品。
抽象工厂模式:解决系列产品问题,生产多种产品。

简单工厂模式:
在这里插入图片描述

namespace Factor{
  public abstract class AbstractMouse{
    public abstract void Print();
  }
}
using UnityEngine;
namespace Factor{
  public class DellMouse: AbstractMouse{
    public override void Print(){
      Debug.Log("生产了一个Dell鼠标");
    }
  }
}
using UnityEngine;
namespace Factor{
  public class HpMouse: AbstractMouse{
    public override void Print(){
      Debug.Log("生产了一个Hp鼠标");
    }
  }
}
namespace Factor{
  public class SampleFactor{
    public AbstractMouse CreateMouse(MouseType emMouseType){
      AbstractMouse mouse = null;
      switch(emMouseType){
        case MouseType.HpMouse:
          mouse = new HpMouse();
          break;
        case MouseType.DellMouse:
          mouse = new DellMouse();
          break;
        default:
          break;
      }
      return mouse;
    }
  }
}
using UnityEngine;
using Factor;

public enum MouseType{
  None,
  DellMouse,
  HpMouse,
}

public class FactorMain:MonoBehaviour{
  void Start(){
    RunNormal();
    RunSampleFactor();
  }
  void RunNormal(){
    DellMouse dellMouse = new DellMouse();
    dellMouse.Print();
    HpMouse hpMouse = new HpMouse();
    hpMosue.Print();
  }
  // 外界不需要关注具体的子类对象
  void RunSampleFactor(){
    SampleFactor factor = new SampleFactor();
    AbstactMouse dellMouse = factor.CreateMouse(MouseType.DellMouse);
    dellMouse.Print();
    AbstactMouse hpMouse = factor.CreateMouse(MouseType.HpMouse);
    hpMouse.Print();
  }

}

工厂模式:
在这里插入图片描述

namespace Factor{
  public abstract class FactorBase{
    public abstract AbstractMouse CreateMouse();
  }
}
namespace Factor{
  public class DellFactor:FactorBase{
    public override AbstractMouse CreateMouse(){
      return new DellMouse();
    }
  }
}
namespace Factor{
  public class HpMouse:FactorBase{
    public override AbstractMouse CreateMouse(){
      return new HpMouse();
    }
  }
}
public class FactorMain:MonoBehaviour{
  void Start(){
    RunFactor();
  }
--snip--
  void RunFactor(){
    DellFactor dellFactor = new DellFactor();
    AbstractMouse dellmouse = dellFactor.CreateMouse();
    dellMouse.Print();

    HpFactor hpFactor = new HpFactor();
    AbstractMouse hpmouse = hpFactor.CreateMouse();
    hpmouse.Print();
  }
}

工厂模式的好处在于当新增一个类的时候我们不会对原来的代码进行修改,只新增类本身及其工厂。
抽象工厂模式: 可以生产多个类别产品。
在这里插入图片描述

namespace Factor{
  public abstract class AbstractKeyBoard{
    public abstract void Print();
  }
}
namespace Factor{
  public class DellKeyBoard:AbstractKeyBoard{
    public override void Print(){
      Debug.Log("Dell键盘");
    }
  }
}
namespace Factor{
  public class HpMouse:AbstractKeyBoard{
    public override void Print(){
      Debug.Log("hp键盘");
    }
  }
}
namespace Factor{
  public abstract class FactorBase{
    public abstract AbstractMouse CreateMouse();
    public abstract AbstractKeyBoard CreateKeyBoard();
  }
}
namespace Factor{
  public class DellFactor:FactorBase{
    public override AbstractMouse CreateMouse(){
      return new DellMouse();
    }
    public override AbstractKeyBoard CreateKeyBoard(){
      return new DellKeyBoard();
    }
  }
}
namespace Factor{
  public class HpMouse:FactorBase{
    public override AbstractMouse CreateMouse(){
      return new HpMouse();
    }
    public override AbstractKeyBoard CreateKeyBoard(){
      return new HpKeyBoard();
    }
  }
}
--snip--
public class FactorMain:MonoBehaviour{
  void Start(){
    RunAbstractFactor();
  }
--snip--
  void RunAbstractFactor(){
    DellFactor dellFactor = new DellFactor();
    AbstractMouse dellmouse = dellFactor.CreateMouse();
    dellMouse.Print();
    AbstractKeyboard dellKeyBoard = dellFactor.CreateKeyBoard();
    dellKeyBoard.Print();

    HpFactor hpFactor = new HpFactor();
    AbstractMouse hpmouse = hpFactor.CreateMouse();
    hpmouse.Print();
    AbstractKeyboard hpKeyBoard = hpFactor.CreateKeyBoard();
    hpKeyBoard .Print();
  }
}

适配器模式

概念:可以用同一种方式调用不同平台上的同一个功能。
在这里插入图片描述

namespace Adaptor{
  public class AndroidLine{
    public void AndroidCharge(){
      Debug.Log("借助Android数据线充电中");
    }
  }
}
namespace Adaptor{
  public class IOSLine{
    public void IOSCharge(){
      Debug.Log("借助IOS数据线充电中");
    }
  }
}
namespace Adaptor{
  public enum AdaptorType{
    None,
    Android,
    IOS,
  }
  public interface IAdaptor{
    void Charge(AdaptorType adaptorType);
  }
  public class Adaptor:IAdaptor{
    AndroidLine androidLine = new AndroidLine();
    IOSLine iosLine = new IOSLine();
    public void Charge(AdaptorType adaptorType){
      if(adaptorType == AdaptorType.Android)
        androidLine.AndroidCharge();
      else if(adaptorType == AdaptorType.IOS){
        iosLine.IOSCharge();
      }
    }
  }
}
namespace Adaptor{
  public class AdaptorMain"MonoBehaviour{
    void Start(){
      // 用的时候统一调用接口即可
      IAdaptor adaptor = new Adaptor();
      adaptor.Charge(AdaptorType.Android);
      adaptor.Charge(AdaptorType.IOS);
    }
  }

}

注意:考虑到如果还有许多代码的情况下,这里接口用于拓展原始类不足的功能。

IO操作

C#字符串

参考:https://www.runoob.com/csharp/csharp-string.html
举例:

public class StringTest{
  void Start(){
    Test1();
  }
  void Test1(){
    string str1 = "XIAOXING";
    string str2 = "xiaoxing";
    Debug.Log(str1.Length); // 长度
    Debug.Log(string.Compare(str1,str2));//相等输出整形0,不等输出1
    Debug.Log(string.Concat(str1,str2));//拼接
    Debug.Log(string.Format("name:{0} age:{1}","张三",14))//格式化输出
    Debug.Log(str1.Contains("I"));//判断是否存在目标字符串
    Debug.Log(str1.IndexOf("I"));//字符串第一次出现的索引
    string [] array = str1.Split(new char[] {'G'});//字符串切割
  }
}

StringBuilder的使用

参考博客:https://blog.csdn.net/sibaison/article/details/72356393
用途:创建新的String的系统开销可能非常安规,如果要修改字符串而不创建新的对象,可以用到这个类。

public class StringBuilderTest{
  void Start(){
    Test1();
  }
  void Test1(){
    stringBuilder sb = new StringBuilder("Hello World",25);//内容和最大长度
    Debug.Log(sb.Length + sb.capacity); //长度+容量
    sb = sb.Append("What a beautiful day!"); // 添加内容
    Debug.Log(sb.ToString());
  }
}

在这里插入图片描述

文件读写

参考链接:http://c.biancheng.net/csharp/file-io.html

using UnityEngine;
public class FileStreamTest:MonoBehaviour{
  void CreateFile(){
    string path = @"E:\WorkSpace\Project\Assets\Resources\student.txt";
    FileStream fileStream = new FileStream(path,FileMode.OpenOrCreate,FileAccess.ReadWrite,FileShare.ReadWrite);
    string msg = "1710026"
    byte[] bytes = Encoding.UTF8.GetBytes(msg);//字符串转换为字节数组
    fileStream.Write(bytes,0,bytes.Length);
    fileStream.Flush();//刷新缓冲区
    fileStream.Close();//关闭流
  }
  void ReadFile(){
    string path = @"E:\WorkSpace\Project\Assets\Resources\student.txt";
    if(File.Exists(path)){
      FileStream fileStream = new FileStream(path,FileMode.Open,FileAccess.Read);
      byte[] bytes = new byte(fileStream.Length);
      fileStream.Read(bytes,0,bytes.Length);
      string s = Encoding.UTF8.GetString(bytes);
      Debug.Log(s)
      fileStream.Close();
    }
    else{
      Console.WriteLine("您查看的文件不存在");
    }
  }
  void WriteAndRead{                         //另一种方法
    // 要写入文件中的数据
    string[] str = new string[]{
        "C语言中文网",
        "http://c.biancheng.net/",
        "C# 教程"
    };
    // 创建 StreamWriter 类的对象
    StreamWriter file = new StreamWriter("demo.txt");
    // 将数组中的数据写入文件
    foreach(string s in str){
        file.WriteLine(s);
    }
    file.Close();
    // 读取文件中的内容
    string line = "";
    StreamReader readfile = new StreamReader("demo.txt");
    while((line = readfile.ReadLine()) != null){
        Console.WriteLine(line);
    }
    readfile.Close();
    Console.ReadKey();
  }
}

用Directory和DirectoryInfo操作文件夹

参考链接:http://c.biancheng.net/csharp/directory.html
一个是静态操作,一个是创建对象后操作

string strDir = @"E:\"
Directory.CreateDirectory(strDir);
Directory.Exists(strDir);  //判断文件夹是否存在
Directory.Delete(strDir);  //删除文件夹,如果有子文件夹需要加参数true
Directory.Move(strDir,@"D:\");//移动文件夹

DirectoryInfo directoryInfo = new DirectoryInfo(strDir);
directoryInfo.Create(); //创建文件夹
directoryInfo.CreateSubdirectory("code-1"); //创建子文件夹
directoryInfo.Delete(); //删除文件夹,如果有子文件夹需要加参数true
directoryInfo.MoveTo("D:\"); // 移动文件夹

数据结构

数组

含义:数组是用来储存数据的集合,元素类型相同,固定长度,顺序集合。

int[] array1 = new int[3];
int[] array2 = new int[3]{1,2,3};
int[] array3 = {1,2,3,4,5};
array1[0] = 5;        //赋值
Debug.Log(array1[0]); //读取
foreach(var v in array2){
  Debug.Log(v);
}

动态数组

动态数组会自动调整大小,可以在指定位置添加删除项目,允许在列表中动态分配内存。

ArrayList arraylist1 = new ArrayList();
arraylist1.Add(45);          //添加一个数据
int[] array3 = {1,2,3,4,5};
arraylist1.AddRange(array3); //添加一组数据
arraylist1.Clear();  //清空
arraylist1.Contains(12);  //判断是否包含特定元素
arraylist1.IndexOf(12); //返回第一次出现的元素索引,没有返回-1
arraylist1.Insert(3,66); //索引+插入元素
arraylist1.Remove(12); // 删除第一次出现的元素
arraylist1.Reverse(); //翻转元素
arraylist1.Sort(); //按顺序排列

列表

和泛型结合使用,作用与功能类似arraylist,无需装箱拆箱,类型转换,提前定义元素类型,比较安全,编译时就能检查错误。

List<int> list1 = new List<int>();
list1.Add(1);
// 和arraylist函数类似

哈希表

哈希表代表了键值对集合,使用键访问集合中的元素。用法比较简单。

Hashtable ht1 = new Hshtable();
ht1.Add("1",99);
ht1.Clear();
if(ht1.ContainsKey("1")){
  Debug.Log("包含键为“1”的数据");
}
ht1.Remove("1");
Debug.Log(ht1["1"]);   //读取
ht1["1"]=99;           //修改
ICollection key = ht1.Keys;
foreach(var k in key){      //遍历
  Debug.Log(ht1[k]);
}

和字典的不同在于可以存储任何类型的变量,不需要提前定义,但需要拆箱装箱。
哈希表本质上是通过数组储存的,一般把Key输入哈希函数中,计算出哈希值,把用这个哈希值作为数组的索引来读取和修改Value。
如果不同的Key计算出了同一个哈希值,称为发生了哈希冲突(哈希碰撞),因此设计合适的哈希函数很重要(直接定址法,数字分析法,折叠法,随机数法和除留余数法等等)
解决哈希冲突的方法有开放寻址法(一个位置被占了就一直找下一个位置),拉链法:通过在数组的位置上额外存放一个指针指向另外的一个位置(相当于链表),然后去下一个位置看是否有冲突,有冲突就继续走向这个位置指针的下一个位置,以此类推。
当哈希表被占领的位置过多(例如超过70%),就会触发扩容机制,创建一个新的数组,长度为原来的两倍,并且重新利用哈希函数计算位置。

字典

字典也存储键值对,但字典也是和泛型一起用的,提前指导好键值的数据类型。字典是通过哈希表实现的。

Dictionary<string,string> dict1 = new Dictionary<string,string>();
dict.add("1","100");
if(dict1.ContainsKey("1")){
  Debug.Log("键存在");
}
dict1["1"] = 100;
foreach(KeyValuePair<string,string> kvp in dict1){
  Debug.Log(kvp.Key + " " + kvp.Value);
}

dict1.Remove("2");
dict1.Clear();

哈希集

包含不重复项的无序集合

HashSet<int> hs1 = new HashSet<int>();
HashSet<int> hs2 = new HashSet<int>();

hs1.Add(1);
hs1.Add(2);
hs1.Add(2); //重复无效
Debug.Log(hs1.count); //计算个数
hs2.Add(2);
hs2.Add(3);
hs1.IntersectWith(hs2);  //h1取交集
hs1.UnionWith(hs2);  // h1取并集
hs1.ExceptWith(hs2);  //h1取差集
hs1.SymmetricExceptWith(hs2); //对称差集,即并集中去掉交集的部分

链表

C#中是有前驱和后驱的双向链表。链表在内存中是离散的,不连续。通过两个指针指向上一个存储位置和下一个存储位置。
链表在删除和插入的时候效率比列表和数组快,但查找的时候只能靠遍历,比较慢。

链表可以分为单向和双向。

LinkedList<int> linList = new LinkedList<int>();
LinkedListNode<int> node;  
node = linList.AddFirst(1); //第一个节点
linlist.AddAfter(node,2); //添加在某节点后面+具体值
linlist.AddBefore(node,0); //添加在某节点前面+具体值
Debug.Log(linList.Count);
Debug.Log(linList.First.Value); //第一个值
Debug.Log(linList.Last.Value); //最后一个值
Debug.Log(node.Previous.Value); //节点前一个值
Debug.Log(node.Next.Value); //节点后一个值

链表逆序(反转链表)的方法:
方法一:就地逆序
方法二:递归法
方法三:插入法

就地逆序:

def reverseList(self,head: Optional[ListNode]) -> Optional[ListNode]:
  pre = None
  cur = head
  while cur:
    nxt = cur.next
    cur.next = pre
    pre = cur
    cur = nxt
  return pre

插入法:
从链表的第二个结点开始,把遍历到的结点插入到头结点的后面,直到遍历结束。假如原链表为head->1->2->3->4->5->6->7,在遍历到2的时候,将2插入到头结点的后面,链表变为head->2->1->3->4->5->6->7,同理head->3->2->1->4->5->6->7等等。

def insertReverse(head):
	# 特殊情况:头节点为空,或者只有单个节点
    if head is None or head.next is None:
        return
    # 操作头节点, 2次指针操作,头节点head.next赋值给cur, 由于头节点最终要成为尾结点,其指向为空
    cur = head.next
    head.next = None
    # 操作第二个及之后的节点,每次遍历到的节点都放到头节点处
    while cur:
        nex = cur.next
        cur.next = head
        head = cur
        cur = nex
    return head

堆栈

先进后出的对象集合

Stack st1 = new Stack();
st1.Push("a");
st1.Push("b");
st1.Push("c");
Debug.Log(st1.Count);
string v = (string)st1.Pop();

v = st1.Peek();  //拿到栈顶的值,不出栈
foreach(var v in st1){
  Debug.Log(v1);
}
// 自己实现,使用链表的形式
class MyStack{
  class StackData{
    public StackData nextItem;
    public object topData;
    public StackData(StackData next,object data){
      this.nextItem = next;
      this.topData = data;
    }
    StackData top;
    public void Push(object data){
      top = new StackData(top,data);
    }
    public object Pop(){
      object rs1 = top.topData;
      top = top.nextItem;
      return rs1;
    }
  }
}
MyStack ms = new MyStack();
ms.Push("a");
ms.Push("b");
ms.Push("c");
string v = ms.Pop();

队列

先进先出的队列集合

Queue queue1 = new Queue();
Queue<int> queue2 = new Queue<int>();  // 更多使用这种,效率高,不用装箱拆箱
queue2.Enqueue(1);    //入队
queue2.Enqueue(2); 
int v = queue2.Dequeue(); 

// 自己实现
class MyQueue{
  class QueueData{
    public QueueData nextItem;
    public object topData;
    public QueueData(QueueData last,object data){
      last.nextItem = this;
      this.topData = data;
    }
    QueueData top;
    QueueData lastData;
    public void Enqueue(object data){
      if(top == null){
        top = new QueneData(data);
      }
      else{
        lastData = new QueueData(lastData,data);
      }
    }
    public object Dequeue(){
      object rs1 = top.topData;
      top = top.nextItem;
      return rs1;
    }
  }
}
Queue mq1 = new MyQueue();
mq1.Enqueue(1);    //入队
mq1.Enqueue(2); 
int v = mq1.Dequeue(); 
int v = mq1.Dequeue(); 

二叉树

一棵二叉树是结点的一个有限集合,该集合或者为空,或者是由一个根节点加上两棵别称为左子树和右子树的二叉树组成。
二叉树的特点:

  • 每个结点最多有两棵子树,即二叉树不存在度大于2的结点。
  • 二叉树的子树有左右之分,其子树的次序不能颠倒。
  • 满二叉树:一个二叉树,如果每一个层的结点数都达到最大值,则这个二叉树就是满二叉树。也就是说,如果一个二叉树的层数为K,且结点总数是(2^k) -1 ,则它就是满二叉树。
  • 完全二叉树:完全二叉树是效率很高的数据结构,完全二叉树是由满二叉树而引出来的。对于深度为K的,有n个结点的二叉树,当且仅当其每一个结点都与深度为K的满二叉树中编号从1至n的结点一一对应时称之为完全二叉树。 要注意的是满二叉树是一种特殊的完全二叉树。
  • 二叉树的顺序存储在物理上是一个数组,在逻辑上是一颗二叉树。
性质
  1. 若规定根节点的层数为1,则一棵非空二叉树的第i层上最多有2^(i-1) 个结点.

  2. 若规定根节点的层数为1,则深度为h的二叉树的最大结点数是2^h- 1.

  3. 对任何一棵二叉树, 如果度为0其叶结点个数为 n0, 度为2的分支结点个数为 n2,则有n0=n2+1

  4. 若规定根节点的层数为1,具有n个结点的满二叉树的深度,h=Log2(n+1). (ps:Log2(n+1)是log以2为
    底,n+1为对数)

  5. 对于具有n个结点的完全二叉树,如果按照从上至下从左至右的数组顺序对所有节点从0开始编号,则对于序号为i的结点有:

    • 若i>0,i位置节点的双亲序号:(i-1)/2;i=0,i为根节点编号,无双亲节点
    • 若2i+1<n,左孩子序号为2i+1,2i+1>=n否则无左孩子
    • 若2i+2<n,右孩子序号为2i+2,2i+2>=n否则无右孩子
链式存储

二叉树的链式存储结构是指,用链表来表示一棵二叉树,即用链来指示元素的逻辑关系。 通常的方法是链表中每个结点由三个域组成,数据域和左右指针域,左右指针分别用来给出该结点左孩子和右孩子所在的链结点的存储地址 。链式结构又分为二叉链和三叉链,当前我们学习中一般都是二叉链,后面课程学到高阶数据结构如红黑树等会用到三叉链。

 // 二叉链
   struct BinaryTreeNode
   {
    struct BinTreeNode* _pLeft; // 指向当前节点左孩子
    struct BinTreeNode* _pRight; // 指向当前节点右孩子
    BTDataType _data; // 当前节点值域
   }
   // 三叉链
   struct BinaryTreeNode
   {
    struct BinTreeNode* _pParent; // 指向当前节点的双亲
    struct BinTreeNode* _pLeft; // 指向当前节点左孩子
    struct BinTreeNode* _pRight; // 指向当前节点右孩子
    BTDataType _data; // 当前节点值域
   }
遍历

参考:https://blog.csdn.net/chinesekobe/article/details/110874773
所谓遍历(Traversal)是指沿着某条搜索路线,依次对树中每个结点均做一次且仅做一次访问。访问结点所做的操作依赖于具体的应用问 题。 遍历是二叉树上最重要的运算之一,是二叉树上进行其它运算之基础。
NLR:前序遍历(Preorder Traversal 亦称先序遍历)——访问根结点的操作发生在遍历其左右子树之前。相当于从顶部开始,逆时针绕圈。
LNR:中序遍历(Inorder Traversal)——访问根结点的操作发生在遍历其左右子树之中(间)。相当于从左往右排序,每个父节点都在其左右子节点的中间。
LRN:后序遍历(Postorder Traversal)——访问根结点的操作发生在遍历其左右子树之后。相当于从左往右,一次从叶子节点取一个元素下来,依次取到顶部。

深度优先遍历:似于先序遍历。
广度优先遍历:按每一层从上到下,从左到右进行遍历。

//	先序遍历
void ShowXianXu(BitTree T)			//		先序遍历二叉树
{
	if(T==NULL)						//	递归中遇到NULL,返回上一层节点
	{
		return;
	}
	printf("%d ",T->data);
	ShowXianXu(T->lchild);			//	递归遍历左子树
	ShowXianXu(T->rchild);			//	递归遍历右子树
}
//	中序遍历
void ShowZhongXu(BitTree T)			//		先序遍历二叉树
{
	if(T==NULL)						//	递归中遇到NULL,返回上一层节点
	{
		return;
	}
	
	ShowZhongXu(T->lchild);			//	递归遍历左子树
	printf("%d ",T->data);
	ShowZhongXu(T->rchild);			//	递归遍历右子树
	
}
//	后序遍历
void ShowHouXu(BitTree T)			//		后序遍历二叉树
{
	if(T==NULL)						//	递归中遇到NULL,返回上一层节点
	{
		return;
	}
	
	ShowHouXu(T->lchild);			//	递归遍历左子树
	ShowHouXu(T->rchild);			//	递归遍历右子树
	printf("%d ",T->data);
}

前序非递归遍历:
这里使用栈来实现,首先将根放进栈中然后定义一个循环,循环条件是栈不为空,首先将栈顶元素弹出,并用一个结点保存,如果栈顶的元素不是空,就输出栈顶元素的数据,然后看它的左右子树,先把右子树压入栈中,再压左子树,(因为栈后进先出,先要访问左子树,所以左子树后进入),每次进入循环如果栈顶为空的化就不进行操作,直接进行操作,如果栈顶不为空的话再进行下面操作。直到所有结点都被遍历。
中序非递归遍历:
还是利用栈,因为先访问最左边的结点,所有我们先定义一个循环一直向左走把左边的所有结点压入栈中,这时候栈顶就是最左侧得结点,我们要将这个接单弹出,然后输出它的值,下来输出的是它的根结点,所以先要把它的右子树都压入栈中,最后再保存根结点就能保证先输出根结点再输出它右侧元素了。
后序非递归遍历:
还是利用栈,和中序非递归类似,先找到最左边的结点,并把这条路径上所有的结点按照找的顺序压入栈中,然后将最左侧结点的数据打印出来,然后判断当前节点是不是栈顶结点的左子树,如果是要先访问右结点才能访问根结点,否则将当前结点置成空,下一次就会输出当前结点的根结点,然后重复上述操作。

void PreOrder(BiNode *bt) { //树的前序遍历
	SqStack s;
	s = InitStack();
	BiNode *p = bt;
	while (p != NULL || StackEmpty(&s) != 1) { //当p为空,栈也为空时退出循环
		while (p != NULL) {
			visit(p->data);//访问根结点
			Push(&s, p); //将指针p的节点压入栈中
			p = p->Lchild; //遍历左子树
		}
		if (StackEmpty(&s) != 1) { //栈不为空
			p = Pop(&s); //根结点出栈,相当于回退
			p = p->rchild; //遍历右子树
		}
	}
	DestroyStack(&s);
}

void MidOrder(BiNode *bt) { //树的中序遍历
	SqStack s;
	s = InitStack();
	BiNode *p = bt;
	while (p != NULL || StackEmpty(&s) != 1) { //当p为空,栈也为空时退出循环
		while (p != NULL) {
			Push(&s, p); //将指针p的节点压入栈中
			p = p->Lchild; //遍历左子树
		}
		if (StackEmpty(&s) != 1) { //栈不为空
			p = Pop(&s); //根结点出栈,相当于回退
			visit(p->data);//访问根结点
			p = p->rchild; //遍历右子树
		}
	}
	DestroyStack(&s);
}

void PostOrder(BiNode *bt) { //树的后序遍历
	SqStack s;
	s = InitStack_1();
	BiNode *p = bt;
	element elem;
	while (p != NULL || StackEmpty_1(&s) != 1) { //当p为空,栈也为空时退出循环
		if (p != NULL) {//第一次入栈,访问左子树
			elem.ptr = p;
			elem.flag = 1; //标记flag为1,表示即将第一次入栈
			Push_1(&s, elem); //将指针p的结点第一次压入栈中
			p = p->Lchild;
		} else {
			elem = Pop_1(&s); //出栈
			p = elem.ptr; //p指向当前要处理的结点
			if (elem.flag == 1) {
				//flag==1时,说明只访问过左子树,还要访问右子树
				elem.flag = 2;
				Push_1(&s, elem); //结点第二次压入栈中
				p = p->rchild;
			} else {
				//flag==2时,左右子树都已经访问过了
				visit(p->data);
				p = NULL; //访问后,p赋为空,确保下次循环时继续出栈(相当于回退)
			}
		}
	}
	DestroyStack_1(&s);
}

反转二叉树

将每个节点的左右节点互换,也就是遍历每一个节点然后交换它们的左右节点,这里就可用到二叉树的各种遍历方法,只是将保存节点值的过程转换为交换左右节点。(中序遍历不能使用,会将某些节点反转两次)

//前序遍历解决问题
public TreeNode invertTree(TreeNode root) {
    if(root==null) return null;
    //反转左右孩子
    TreeNode temp=root.left;
    root.left=root.right;
    root.right=temp;
    invertTree(root.left); //左
    invertTree(root.right); //右
    return root;
}
排序二叉树找第k大节点

题目:

输入: root = [3,1,4,null,2], k = 1
   3
  / \
 1   4
  \
   2
输出: 4

示例 2:
输入: root = [5,3,6,2,4,null,null,1], k = 3
       5
      / \
     3   6
    / \
   2   4
  /
 1
输出: 4
 
限制:
1 ≤ k ≤ 二叉搜索树元素个数

答案:

class Solution {
public:
    int kthLargest(TreeNode* root, int k) {
        //右根左降序遍历
        stack<TreeNode*> s;
        while(root || !s.empty())
        {
        	while(root)
        	{
        		s.push(root);
        		root = root->right;
        	}
        	root = s.top();
        	s.pop();
        	k--;
        	if(k==0)
        		return root->val;
        	root = root->left;
        }
        return -1;
    }
};

在这段代码中,使用右根左的遍历顺序是为了实现降序遍历二叉搜索树(Binary Search Tree)。降序遍历意味着按照节点值的降序来访问节点。
对于二叉搜索树而言,中序遍历可以实现升序遍历,即从小到大访问节点。而右根左的遍历顺序是中序遍历的逆序,所以可以实现从大到小访问节点,即降序遍历。
在这个特定的问题中,要找到二叉搜索树中第 k 大的元素。通过降序遍历,我们可以依次访问每个节点的值,并在第 k 个节点时返回对应的值,即为第 k 大的元素。
使用栈来辅助遍历的过程,可以实现迭代方式的降序遍历。从根节点开始,先将右子树的节点依次入栈,然后弹出栈顶元素,进行相应的处理(比如减小 k 的值,判断是否为第 k 大的元素),然后将左子节点作为下一个要处理的节点。
因此,使用右根左的遍历顺序是为了满足降序遍历的需求,并能够有效地找到第 k 大的元素。

平衡二叉树

二叉树能提高查询的效率 O(logn),但是当你插入{1,2,3,4,5,6} 这种数据的时候,你的二叉树就像一个链表一样,搜索效率变为 O(n)

所以一般的二叉搜索树在进行数据的存储时可能会使工作的时间复杂度并未降低太多,这时便需要我们将二叉树的高度尽可能的降低,使二叉树的高度降低为log(n),这样我们就得到了一个高度平衡的二叉搜索树,使对于数据的一系列操作的时间复杂度稳定在 O(logn)。
红黑树就是一颗自平衡的二叉树。

跳表

跳表(Skip List)是一种用于实现有序集合的数据结构,它通过在一个有序链表的基础上添加多层索引来提高数据的查找效率。跳表允许快速地按照元素的键值进行查找、插入和删除操作。
跳表的核心思想是通过建立多级索引层次,使得较大范围的元素可以通过跳过一些节点来进行快速搜索。每一级索引都是原始链表的一个子集,且比前一级索引稀疏,即包含更少的节点。最底层的索引就是原始的有序链表。
在跳表中,每个节点包含一个键值对,键用于按照有序方式进行排序,值则是具体存储的数据。每个节点还包含指向下一级索引节点的指针,使得可以在不同层次的索引之间进行快速的跳跃。

查找:从顶层往下,如果下一个指向的节点索引大于我们需要查找的索引,那么就跳到下一层。
插入:当我们在跳表中插入数据的时候,我们通过选择同时将这个数据插入到部分索引层中,如何选择索引层,可以通过一个随机函数来决定这个节点插入到哪几级索引中,比如随机生成了k,那么就将这个索引加入到,第一级到第k级索引中。
删除:和链表插入删除一致,不过是多了几层

public Node find(E e) {
    if (empty()) {
        return null;
    }
    return find(e, head, curLevel);
}

private Node find(E e, Node current, int level) {
    while (level >= 0) {
        current = findNext(e, current, level);
        level--;
    }
    return current;
}

//返回给定层数中小于e的最大者
private Node findNext(E e, Node current, int level) {
    Node next = current.next(level);
    while (next != null) {
        if (e.compareTo(next.data) < 0) {
            break;
        }
        //到这说明e >= next.data
        current = next;
        next = current.next(level);
    }
    return current;
}

跳表的查询效率是时间复杂度是 nlogn

红黑树

参考:https://blog.csdn.net/xiaofeng10330111/article/details/106080394
红黑树(Red-Black Tree)是一种自平衡的二叉搜索树,它在插入和删除操作后能够自动调整树的结构,保持树的平衡性。红黑树是在普通二叉搜索树的基础上增加了一些附加的规则和性质来确保平衡。

红黑树的特点包括:

  1. 根节点是黑色的。
  2. 每个叶子节点(NIL 节点,空节点)是黑色的。
  3. 如果一个节点是红色的,那么它的两个子节点都是黑色的。
  4. 对于每个节点,从该节点到其所有后代叶子节点的简单路径上,均包含相同数量的黑色节点。

这些特性保证了红黑树的关键性质,包括:
任意节点到其每个叶子节点的路径长度相等或差至多为1,因此树的高度是对数级别的,保持了较好的平衡性。
查找、插入和删除操作的时间复杂度为 O(log n),保持了较高的操作效率。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1189269.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Figma转Sketch文件教程,超简单!

相信大家做设计的都多多少少听过一点Figma和Sktech&#xff0c;这2个设计软件是目前市场上很受欢迎的专业UI设计软件&#xff0c;在全球各地都有很多粉丝用户。但是相对来说&#xff0c;Figma与Sketch只支持iOS系统有所不同&#xff0c;Figma是一个在线设计软件&#xff0c;不限…

Flink(二)【Flink 部署模式】

前言 今天是Flink学习的第二天&#xff0c;我的心情异常的复杂哈哈哈&#xff08;苦笑&#xff09;&#xff0c;学习上还是比较顺利的&#xff0c;感情上我并不擅长&#xff0c;所以心情波动大在所难免。害&#xff0c;至少还有学习让我不被各种糟糕琐碎的日常生活里的人和事所…

【2023方案开发】无刷电机自动打气泵方案

如今新能源车大行其道&#xff0c;而车厂在考虑成本和设计的情况下&#xff0c;将备胎给取消了&#xff0c;那假如在车胎气压不足的状态下&#xff0c;没有备胎更换&#xff0c;那将如何解决困境。气压不住需要打气&#xff0c;那配备一台打气泵尤为重要&#xff0c;即使常年可…

Git系列之Git入门级(带你走进Git的世界)

&#x1f389;&#x1f389;欢迎来到我的CSDN主页&#xff01;&#x1f389;&#x1f389; &#x1f3c5;我是君易--鑨&#xff0c;一个在CSDN分享笔记的博主。&#x1f4da;&#x1f4da; &#x1f31f;推荐给大家我的博客专栏《Git实战开发》。&#x1f3af;&#x1f3af; &a…

论文速览 MobiCom 2023 | NeRF2 : Neural Radio-Frequency Radiance Fields

注1:本文系“最新论文速览”系列之一,致力于简洁清晰地介绍、解读最新的顶会/顶刊论文 Xiaopeng Zhao, Zhenlin An, Qingrui Pan, and Lei Yang. 2023. NeRF2: Neural Radio-Frequency Radiance Fields. In Proceedings of the 29th Annual International Conference on Mobil…

AIGC:使用生成对抗网络GAN实现MINST手写数字图像生成

1 生成对抗网络 生成对抗网络&#xff08;Generative Adversarial Networks, GAN&#xff09;是一种非常经典的生成式模型&#xff0c;它受到双人零和博弈的启发&#xff0c;让两个神经网络在相互博弈中进行学习&#xff0c;开创了生成式模型的新范式。从 2017 年以后&#x…

kubectl声明式资源管理命令

一、声明式资源管理介绍&#xff1a; 适合于对资源的修改操作声明式资源管理方法依赖于资源配置清单文件对资源进行管理资源配置清单文件有两种格式&#xff1a;yaml&#xff08;人性化&#xff0c;易读&#xff09;&#xff0c;json&#xff08;易于api接口解析&#xff09;对…

如何排查CPU飙升的问题所在

我们在线上排查问题的时候发现系统的CPU直接爆红了,如何排查问题呢?怎么快速定位问题的所在呢? linux系统下: 首先我们通过top命令来查看那个进程占用CPU过大: top 这里只是简单的模拟一下,假设java进程占用CPU很大,那么我们通过这个进程来查找进程下的线程使用CPU的情况…

【23真题】C9无歧视,专业课均分130!

今天分享的是23年哈尔滨工业大学803的信号与系统部分的试题及解析。 本套试卷难度分析&#xff1a;22年哈今天分享的是23年哈尔滨工业大学803的信号与系统部分的试题及解析。 哈尔滨工业大学803考研真题&#xff0c;我也发布过&#xff0c;若有需要&#xff0c;戳这里自取&…

6-爬虫-scrapy解析数据(使用css选择器解析数据、xpath 解析数据)、 配置文件

1 scrapy解析数据 1.1 使用css选择器解析数据 1.2 xpath 解析数据 2 配置文件 3 整站爬取博客–》爬取详情–》数据传递 scrapy 爬虫框架补充 # 1 打码平台---》破解验证码-数字字母&#xff1a;ddddocr-计算题&#xff0c;滑块&#xff0c;成语。。。-云打码&#xff0c;超…

文件上传 [GXYCTF2019]BabyUpload1

打开题目 传个是jpg文件后缀的一句话木马上去 代码如下 <script languagephp>eval($_POST[v]);</script> 发现上传成功 因此我们需要先上传 .htaccess 文件&#xff0c;然后再上传 2.jpg文件 .htaccess作用&#xff1a;文件将别的后缀名文件内容解析为php程序…

使用matlab实现图像信号的色彩空间转换

利用matlab对图像信号进行读取&#xff0c;并对RGB空间进行转换&#xff0c;如转换到HSI空间等。 下面的这个代码是在使用了rgb2hsi()方法失败后&#xff0c;进行修改的。 rgb2hsi(img)这个方法可以将RGB图像转换为HIS图像&#xff1b;但是爆出了 Untitled5(line 5)hsi rgb2h…

通过Cookie和Session来实现网站中登录账号的功能

文章目录 一、Cookie和Session二、基于Cookie和Session实现登录账号的功能2.1步骤一2.2步骤二2.3步骤三2.4总结通过Cookie和Session来实现登录功能2.5运行截图 一、Cookie和Session cookie是http请求header中的一个属性&#xff0c;是浏览器持久化存储数据的一种机制&#xff…

2023年香港专才计划(输入内地人才计划)拿身份最新申请攻略!

2023年香港专才计划&#xff08;输入内地人才计划&#xff09;拿身份最新申请攻略&#xff01; 近年来&#xff0c;香港受持续的人口老龄化等多因素影响&#xff0c;2022年香港人口总计减少了约12.17万人&#xff0c;跌幅1.6%&#xff0c;其中净移出人数约9.5万人。在此背景下&…

web3 从redux中拿出所有已完成订单 并渲染到对应的Table列表中

上文web3 React dapp项目通过事件从区块链中拿到 已取消 已完成 和所有的订单数据 并存入redux中 中 我们已经从 区块中拿到了自己的订单 然后 我们恢复一下上文的环境 ganache ganache -d然后 登一下 MetaMask 然后 用我们的项目 发布一下合约 truffle migrate --reset然后…

汽车标定技术(八)--MPC57xx是如何支持标定的页切换

目录 1.页切换的概念 1.1 标定常量的理解 1.2 页切换 2.MPC57xx的Overlay模块 3.小结 1.页切换的概念 在汽车标定测量中&#xff0c;有一个概念我想很多人都听过&#xff0c;但是实际上在项目里没有用到过&#xff0c;那就是今天要讲的页切换概念。在讲页切换的时候&#…

自考改革过渡期!广东小自考最优解只需要2门笔试

图片来源&#xff1a;广东省考试院* 近期广东教育考试院公布了自考专业调整的相关通知&#xff0c;新的专业考试计划从2026年1月起执行。 这次改革过渡期中有一个重大利好消息&#xff0c;小自考专业笔试统考科目最少只需考2门笔试&#xff01; 这是为什么呢&#xff1f; 小…

蓝牙安全管理(SM:Security Manager)规范详解

总述 配对(Pairing)分为三个阶段&#xff0c;前两个阶段是必须的&#xff0c;而第三阶段是可选的&#xff0c;三个阶段如下&#xff1a; 阶段1&#xff1a;配对功能交换(Pairing Feature Exchange) 阶段2(LE传统配对 LE legacy pairing)&#xff1a;短期密钥(STK:Short Term…

阿里巴巴国际站为什么凉了?数字一体化方案崛起!

随着全球化和数字化的浪潮不断涌现&#xff0c;跨境电商市场一直以惊人的速度增长。中国作为主要的出口和进口市场之一&#xff0c;成为跨境贸易的重要参与者。然而&#xff0c;近年来&#xff0c;阿里巴巴国际站似乎面临了一些挑战&#xff0c;同时数字一体化方案崭露头角&…

FallO’ween 活动:元宇宙中秋天与万圣节的邂逅

秋风徐徐&#xff0c;万圣节已经来临&#xff01;还有什么比沉浸在 FallOween 的世界更好的方式来庆祝这个“恐怖“的季节呢&#xff1f;这项全新的季节性活动将于 10 月 30 日推出&#xff0c;将秋季的迷人气息与万圣节的怪异魅力融为一体&#xff0c;为大家带来为期 30 天的冒…