CSharp 进阶
ArrayList 知识点
知识点一 ArrayList 的本质
ArrayList 是一个 C#为我们封装好的类,它的本质是一个 object 类型的数组。
ArrayList 类帮助我们实现了很多方法,比如数组的增删查改。
知识点二 声明
需要引用命名空间 using System.Collections;
1 | ArrayList array = new ArrayList(); |
知识点三 增删查改
增
1 | public virtual int Add(object? value); //添加一个元素 |
删
1 | public virtual void Remove(object? obj); //删除指定元素 |
查
1 | public virtual object? this[int index] { get; set; } //索引器 |
改
1 | array[0] = "999"; //直接使用索引器进行修改 |
遍历
1 | public virtual int Count { get; } //属性,返回元素个数 |
知识点四 装箱拆箱
ArrayList 本质上是一个可以自动扩容的 object 数组,由于用万物之父来存储数据,自然存在装箱拆箱
当往其中进行值类型存储时就是在装箱,当将值类型对象取出来转换使用时,就存在拆箱
所以 ArrayList 尽量少用,之后我们会学习更好的数据容器。
1 | int k = 1; |
练习题一:请简述 ArrayList 和数组的区别
ArrayList 本质上是一个 object 数组的封装
- ArrayList 可以不用一开始就定长,单独使用数组是定长的
- 数组可以指定存储类型,ArrayList 默认为 object 类型
- 数组的增删需要我们自己去实现,ArrayList 帮我们封装了方便的 API 来使用
- ArrayList 使用时可能存在装箱拆箱,数组使用时只要不是 object 数组那就不存在这个问题
- 数组长度为 Length, ArrayList 长度为 Count
Stack 知识点
知识点一 Stack 的本质
Stack(栈)是一个 C#为我们封装好的类
它的本质也是 object[]数组,只是封装了特殊的存储规则
Stack 是栈存储容器,栈是一种先进后出的数据结构
先存入的数据后获取,后存入的数据先获取
栈是先进后出
知识点二 声明
需要引用命名空间 System.Collections
1 | Stack stack = new Stack(); |
知识点三 增取查改
增
1 | public virtual void Push(object obj); //入栈 |
取
栈中不存在删除的概念,只有取出栈的概念
1 | public virtual object Pop(); //出栈,删除栈顶元素,并返回这个元素 |
查
1 | //栈无法查看指定位置的 元素,只能查看栈顶的内容 |
改
栈无法改变其中的元素 只能压(存)和弹(取),实在要改 只有清空
1 | public virtual void Clear(); //清空栈 |
知识点四 遍历
长度
1 | public virtual int Count { get; } //属性,返回栈中元素个数 |
用 foreach 遍历,遍历出来的顺序 也是从栈顶到栈底
1 | foreach(object item in stack) |
还有一种遍历方式,将栈转换为 object 数组,遍历出来的顺序 也是从栈顶到栈底
1 | object[] array = stack.ToArray(); |
循环弹栈遍历
1 | while( stack.Count > 0 ) |
知识点五 装箱拆箱
由于用万物之父来存储数据,自然存在装箱拆箱。
当往其中进行值类型存储时就是在装箱。
当将值类型对象取出来转换使用时,就存在拆箱。
Queue 知识点
知识点一 Queue 本质
Queue 是一个 C#为我们封装好的类,它的本质也是 object[]数组,只是封装了特殊的存储规则
Queue 是队列存储容器,队列是一种先进先出的数据结构,先存入的数据先获取,后存入的数据后获取。
先进先出
知识点二 声明
需要引用命名空间 System.Collections
1 | Queue queue = new Queue(); |
知识点三 增取查改
增
1 | public virtual void Enqueue(object obj); //入队 |
取
队列中不存在删除的概念,只有取的概念 取出先加入的对象
1 | public virtual object Dequeue(); //取出队头元素并删除,然后返回 |
查
1 | //查看队列头部元素但不会移除 |
改
队列无法改变其中的元素 只能进出队列,实在要改 只有清空
1 | public virtual void Clear(); |
知识点四 遍历
长度
1 | public virtual int Count { get; } |
用 foreach 遍历
1 | foreach (object item in queue) |
将队列转换为 object 数组遍历
1 | object[] array = queue.ToArray(); |
循环出队遍历
1 | while(queue.Count>0) |
知识点五 装箱拆箱
由于用万物之父来存储数据,自然存在装箱拆箱。
当往其中进行值类型存储时就是在装箱。
当将值类型对象取出来转换使用时,就存在拆箱。
Hashtable 知识点
知识点一 Hashtalbe 的本质
Hashtable(又称散列表) 是基于键的哈希代码组织起来的 键/值对
它的主要作用是提高数据查询的效率
使用键来访问集合中的元素
知识点二 声明
需要引用命名空间 System.Collections
1 | Hashtable hashtable = new Hashtable(); |
知识点三 增删查改
增
注意:不能出现相同键
1 | public virtual void Add(object key, object? value); //添加一组键值对 |
删:只能通过键去删除;删除不存在的键 没反应;或者直接清空
1 | public virtual void Remove(object key); |
查
1 | public virtual object? this[object key] { get; set; } //索引器,通过键查找值 |
改:只能改 键对应的值内容 无法修改键
1 | hashtable[1] = 100.5f; //通过索引器修改 |
知识点四 遍历
得到键值对 对数
1 | public virtual int Count { get; } //属性,返回键值对的个数 |
遍历所有键
1 | foreach (object item in hashtable.Keys) |
遍历所有值
1 | foreach (object item in hashtable.Values) |
键值对一起遍历
1 | foreach (DictionaryEntry item in hashtable) |
迭代器遍历法
1 | IDictionaryEnumerator myEnumerator = hashtable.GetEnumerator(); |
知识点五 装箱拆箱
由于用万物之父来存储数据,自然存在装箱拆箱
当往其中进行值类型存储时就是在装箱
当将值类型对象取出来转换使用时,就存在拆箱
泛型 知识点
知识点一 泛型是什么
泛型实现了类型参数化,达到代码重用目的
通过类型参数化来实现同一份代码上操作多种类型
泛型相当于类型占位符
定义类或方法时使用替代符代表变量类型
当真正使用类或者方法时再具体指定类型
知识点二 泛型分类
泛型类和泛型接口
基本语法:
1 | class 类名<泛型占位字母> |
泛型函数
1 | //基本语法:函数名<泛型占位字母>(参数列表) |
注意:泛型占位字母可以有多个,用逗号分开
知识点三 泛型类和接口
1 | class TestClass<T> |
1 | TestClass<int> t = new TestClass<int>(); |
知识点四 泛型方法
普通类中的泛型方法
1 | class Test2 |
泛型类中的泛型方法
1 | class Test2<T> |
1 | Test2 tt = new Test2(); |
知识点五 泛型的作用
- 不同类型对象的相同逻辑处理就可以选择泛型
- 使用泛型可以一定程度避免装箱拆箱
举例:优化 ArrayList
1 | class ArrayList<T> |
总结
- 声明泛型时 它只是一个类型的占位符
- 泛型真正起作用的时候 是在使用它的时候
- 泛型占位字母可以有 n 个用逗号分开
- 泛型占位字母一般是大写字母
- 不确定泛型类型时 获取默认值 可以使用 default(占位字符)
- 看到<>包裹的字母 那肯定是泛型
泛型约束 知识点
知识点一 什么是泛型约束
让泛型的类型有一定的限制,关键字:where
泛型约束一共有 6 种:
约束 | 关键字 |
---|---|
值类型 | where 泛型字母:struct |
引用类型 | where 泛型字母:class |
存在无参公共构造函数 | where 泛型字母:new() |
某个类本身或者其派生类 | where 泛型字母:类名 |
某个接口的派生类型 | where 泛型字母:接口名 |
另一个泛型类型本身或者派生类型 | where 泛型字母:另一个泛型字母 |
知识点二 各泛型约束讲解
值类型约束
约束类型 T 必须是值类型
1 | class Test1<T> where T:struct |
引用类型约束
约束类型 T 必须是引用类型
1 | class Test2<T> where T:class |
公共无参构造约束
约束类型 T 必须拥有公告无参构造函数(即可实例化,可 new)
1 | class Test3<T> where T:new() |
类约束
约束类型 T 必须是某个类或者这个类的子类
1 | class Test4<T> where T : Test1 |
接口约束
约束类型必须是这个接口的子类
1 | interface IFly |
另一个泛型约束
约束类型 T 是 U 本身或者 U 的子类
1 | class Test6<T,U> where T : U |
知识点三 约束的组合使用
约束可以组合使用,但有时不能同时存在两个约束,或者两个约束的顺序有次序
比如,new()约束一般写在其他约束前面。
1 | class Test7<T> where T: class,new() |
知识点四 多个泛型有约束
1 | class Test8<T,K> where T:class,new() where K:struct |
总结
泛型约束:让类型有一定限制
六种约束:class,struct,new(),类名,接口名,另一个泛型字母
注意:
- 可以组合使用
- 多个泛型约束 用 where 连接即可
List 知识点
知识点一 List 的本质
List 是一个 C#为我们封装好的类,它的本质是一个可变类型的泛型数组。
List 类帮助我们实现了很多方法,比如泛型数组的增删查改。
知识点二 声明
需要引用命名空间
1 | using System.Collections.Generic; |
1 | List<int> list = new List<int>(); |
知识点三 增删查改
增
1 | public void Add(T item); //添加一个元素 |
删
1 | public bool Remove(T item); |
查
1 | public T this[int index] { get; set; } //索引器 |
改:使用索引器修改
知识点四 遍历
长度和容量
1 | public int Count { get; } |
直接遍历
1 | for (int i = 0; i < list.Count; i++) |
foreach 遍历
1 | foreach (int item in list) |
Dictionary 知识点
知识点一 Dictionary 的本质
可以将 Dictionary 理解为 拥有泛型的 Hashtable;
它也是基于键的哈希代码组织起来的 键/值对;
键值对类型从 Hashtable 的 object 变为了可以自己制定的泛型。
知识点二 声明
需要引用命名空间 using System.Collections.Generic
1 | Dictionary<int, string> dictionary = new Dictionary<int, string>(); |
知识点三 增删查改
增
注意:不能出现相同键
1 | public void Add(TKey key, TValue value); |
删:只能通过键去删除,删除不存在键 没反应。
1 | public bool Remove(TKey key); |
查
1 | //通过键查看值,找不到直接报错,和HashTable不一样,不会返回null了 |
改:通过索引器修改
知识点四 遍历
遍历所有键
1 | foreach (int item in dictionary.Keys) |
遍历所有值
1 | foreach (string item in dictionary.Values) |
键值对一起遍历
1 | foreach (KeyValuePair<int,string> item in dictionary) |
顺序存储和链式存储 知识点
知识点一 数据结构
数据结构:
数据结构是计算机存储、组织数据的方式(规则);
数据结构是指相互之间存在一种或多种特定关系的数据元素的集合;
比如自定义的一个 类 也可以称为一种数据结构 自己定义的数据组合规则
不要把数据结构想的太复杂,简单点理解,就是人定义的 存储数据 和 表示数据之间关系 的规则而已
常用的数据结构(前辈总结和制定的一些经典规则)
数组、栈、队列、链表、树、图、堆、散列表
知识点二 线性表
线性表是一种数据结构,是由 n 个具有相同特性的数据元素的有限序列
比如数组、ArrayList、Stack、Queue、链表等等
顺序存储和链式存储 是数据结构中两种 存储结构
知识点三 顺序存储
数组、Stack、Queue、List、ArrayList —— 顺序存储
只是 数组、Stack、Queue 的 组织规则不同而已
顺序存储:用一组地址连续的存储单元依次存储线性表的各个数据元素
知识点四 链式存储
单向链表、双向链表、循环链表 —— 链式存储
链式存储(链接存储):用一组任意的存储单元存储线性表中的各个数据元素
知识点五 自己实现一个最简单的单向链表
这里实现的链表有 bug,没有处理移除尾结点的情况。
1 | /// <summary> |
知识点六 顺序存储和链式存储的优缺点
从增删查改的角度去思考:
增:链式存储 计算上 优于顺序存储 (中间插入时链式不用像顺序一样去移动位置)
删:链式存储 计算上 优于顺序存储 (中间删除时链式不用像顺序一样去移动位置)
查:顺序存储 使用上 优于链式存储 (数组可以直接通过下标得到元素,链式需要遍历)
改:顺序存储 使用上 优于链式存储 (数组可以直接通过下标得到元素,链式需要遍历)
练习题三-实现双向链表
1 | class LinkedNode<T> |
LinkedList 知识点
知识点一 LinkedList
LinkedList 是一个 C#为我们封装好的类,它的本质是一个可变类型的泛型双向链表
知识点二 声明
需要引用命名空间
1 | using System.Collections.Generic; |
1 | LinkedList<int> linkedList = new LinkedList<int>(); |
链表对象 需要掌握两个类
一个是链表本身 一个是链表节点类 LinkedListNode
知识点三 增删查改
增
1 | public LinkedListNode<T> AddLast(T value); //尾部添加元素 |
删
1 | public void RemoveFirst(); //移除头结点 |
查
1 | public LinkedListNode<T>? First { get; } //头结点 |
改:要先得再改 得到节点 再改变其中的值
1 | Console.WriteLine(linkedList.First.Value); |
知识点四 遍历
LinkedListNode
1 | public LinkedListNode<T>? Next { get; } //属性,上一个节点 |
foreach 遍历
1 | foreach (int item in linkedList) |
通过节点遍历
1 | //从头到尾 |
泛型栈和队列 知识点
知识点一 回顾数据容器
变量
1 | //无符号 |
复杂数据容器
1 | //枚举 enum |
数据集合(容器)
1 | //using System.Collections; |
泛型数据集合
1 | //using System.Collections.Generic; |
知识点二 泛型栈和队列
命名空间:using System.Collections.Generic;
使用上 和之前的 Stack 和 Queue 一模一样
1 | Stack<int> stack = new Stack<int>(); |
练习题
自己总结一下,数组、List、Dictionary、Stack、Queue、LinkedList
这些存储容器,对于我们来说应该如何选择他们来使用
普通线性表:
数组,List,LinkedList
数组:固定的不变的一组数据
List: 经常改变,经常通过下标查找
LinkedList:不确定长度的,经常临时插入改变,查找不多
先进后出:
Stack
对于一些可以利用先进后出存储特点的逻辑
比如:UI 面板显隐规则
先进先出:
Queue
对于一些可以利用先进先出存储特点的逻辑
比如:消息队列,有了就往里放,然后慢慢依次处理
键值对:
Dictionary
需要频繁查找的,有对应关系的数据
比如一些数据存储 id 对应数据内容
道具 ID ——> 道具信息
怪物 ID ——> 怪物对象
等等
委托 知识点
C# 的委托与事件具体是怎么一回事_哔哩哔哩_bilibili
知识点一 委托是什么
委托是 函数(方法)的容器 ,可以理解为表示函数(方法)的变量类型
用来 存储、传递函数(方法)
委托的本质是一个类,用来定义函数(方法)的类型(返回值和参数的类型)
不同的 函数(方法)必须对应和各自”格式”一致的委托
知识点二 基本语法
关键字 : delegate
语法:访问修饰符 delegate 返回值 委托名(参数列表);
写在哪里?
可以声明在 namespace 和 class 语句块中
更多的写在 namespace 中
简单记忆委托语法 就是 函数申明语法前面加一个 delegate 关键字
知识点三 定义自定义委托
访问修饰默认不写 为 public 在别的命名空间中也能使用
private 其它命名空间就不能用了,一般使用 public
1 | //申明了一个可以用来存储无参无返回值函数的容器 |
知识点四 使用定义好的委托
记住,委托变量是函数的容器
注意:委托变量只能装载与本委托声明时,格式一致的函数。这里的格式是指返回值与参数
委托常用在:
- 作为类的成员
- 作为函数的参数
直接使用委托变量:
注意:使用委托,会将容器中的所有函数全部都调用,调用的顺序是添加的顺序
1 | //定义了一个委托变量f,它的格式是无参无返回值 |
知识点五 委托变量可以存储多个函数(多播委托)
增加委托:
1 | public void AddFun(MyFun fun, MyFun2 fun2) |
注意:也可以直接加一个函数
1 | MyFun ff = null; |
删除委托:
同样的,可以直接减一个函数,指定删除
1 | public void RemoveFun(MyFun fun, MyFun2 fun2) |
知识点六 系统定义好的委托
使用系统自带委托 需要引用 using System;
参数修饰符:in 协变为参数,out 逆变为返回值。
Action:无返回值
1 | Action action = Fun; |
Func:有返回值,最后一个类型为返回值。
1 | //定义了一个Func委托变量,这个委托的参数有两个 |
总结
简单理解 委托 就是装载、传递函数的容器而已
可以用委托变量 来存储函数或者传递函数的
系统其实已经提供了很多委托给我们用
Action:没有返回值,参数提供了 0~16 个委托给我们用
Func:有返回值,参数提供了 0~16 个委托给我们用
事件 知识点
知识点一 事件是什么
事件是基于委托的存在,事件是委托的安全包裹;
让委托的使用更具有安全性,事件 是一种特殊的变量类型。
可以说,事件,就是类中的委托。
知识点二 事件的使用
声明语法:
1 | 访问修饰符 event 委托类型 事件名; |
事件的使用:
- 事件是作为 成员变量存在于类中
- 委托怎么用 事件就怎么用
事件相对于委托的区别:
- 不能在类外部 赋值,即不能使用=,但是可以在类外部+=,-=
- 不能再类外部 调用,但是可以通过封装再调用
- 事件 是不能作为临时变量在函数中使用的!
注意:它只能作为成员存在于类和接口以及结构体中
1 | class Test |
1 | //事件是不能再外部赋值的 |
知识点三 为什么有事件
- 防止外部随意置空委托
- 防止外部随意调用委托
- 事件相当于对委托进行了一次封装 让其更加安全
总结
事件和委托的区别:事件和委托的使用基本是一模一样的,事件就是特殊的委托(类中的委托)
主要区别:
- 事件不能再外部使用赋值=符号,只能使用+ - 委托 哪里都能用
- 事件 不能再外部执行 委托哪里都能执行
- 事件 不能作为 函数中的临时变量的 委托可以
匿名函数 知识点
知识点一 什么是匿名函数
顾名思义,就是没有名字的函数
匿名函数的使用主要是配合委托和事件进行使用
脱离委托和事件 是不会使用匿名函数的
知识点二 基本语法
1 | delegate (参数列表) |
何时使用?
- 函数中传递委托参数时
- 委托或事件赋值时
知识点三 使用
它的使用就是委托或者事件的使用,区别是,一声明匿名函数就必须保存到委托或者事件中。
无参无返回
注意:由于匿名函数是匿名的,所以,要使用它,必须保存到委托或者事件中!
这样声明匿名函数 只是在声明函数而已 还没有调用
真正调用它的时候 是这个委托容器啥时候调用 就什么时候调用这个匿名函数
1 | Action A = delegate () |
有参
1 | Action<int, string> b = delegate (int a, string b) |
有返回值
注意,这个返回值是通过委托 Func 控制的
1 | Func<string> c = delegate () |
一般使用情况
一般情况会作为函数参数传递 或者 作为函数返回值
1 | class Test |
1 | //作函数参数 |
知识点四 匿名函数的缺点
添加到委托或事件容器中后 不记录 无法单独移除
因为匿名函数没有名字 所以没有办法指定移除某一个匿名函数
注意:同样逻辑的匿名函数,不是同一个函数。
1 | //此匿名函数 非彼匿名函数 不能通过看逻辑是否一样就证明是一个 |
总结
匿名函数 就是没有名字的函数
固定写法:
1 | delegate(参数列表){} |
使用:主要是在 委托传递和存储时 为了方便可以直接使用该匿名函数
缺点是 没有办法指定移除
lambda 表达式 知识点
知识点一 什么是 lambda 表达式
可以将 lambad 表达式 理解为匿名函数的简写。
它除了写法不同外,使用上和匿名函数一模一样,都是和委托或者事件 配合使用的。
知识点二 lambda 表达式语法
1 | //匿名函数 |
知识点三 使用
无参无返回
1 | Action a = () => |
有参
1 | Action<int> a2 = (int value) => |
甚至参数类型都可以省略 参数类型和委托或事件容器一致
1 | Action<int> a3 = (value) => |
有返回值
1 | Func<string, int> a4 = (value) => |
其它传参使用等和匿名函数一样
缺点也是和匿名函数一样的
知识点四 闭包
内层的函数可以引用包含在它外层的函数的变量;
即使外层函数的执行已经终止
注意:该变量提供的值并非变量创建时的值,而是在父函数范围内的最终值。
1 | class Test |
注意,上方程序中的 index 值是 10,因为 i 的最终值是 10
委托 补充知识点
有返回值的委托存储多个函数 调用时如何获取多个返回值?
问题:当有返回值的委托容器,存储多个函数时,我们想要获取所有的返回值
1 | Func<string> funTest = () => { |
上述程序如果直接调用,只会输出一个 3,因为第三个函数是最后执行的,也就只返回最后一个函数的返回值。
为了解决这个问题,我们可以遍历委托容器中的每个函数
使用**Func.GetInvocationList()**函数获取容器中的每个函数,然后使用 foreach 遍历就可以了。
1 | foreach (Func<string> func in funTest.GetInvocationList()) { |
List 排序 知识点
知识点一 List 自带排序方法
list 提供了排序方法
1 | public void Sort(); //默认升序 |
知识点二 自定义类的排序
如果需要自定义类支持排序,类需要继承接口 IComparable<ClassName>
并且实现接口
1 | public int CompareTo(ClassName other); |
CompareTo 函数
在这个函数当中,我们需要处理排序逻辑,通过这个函数的返回值,决定排序的规则。
返回值的含义:
小于 0:放在传入对象的前面
等于 0:保持当前的位置不变
大于 0:放在传入对象的后面
可以简单理解 传入对象的位置 就是 0;
如果你的返回为负数 就放在它的左边 也就前面;
如果你返回正数 就放在它的右边 也就是后面;
注意,这里作为参照的对象是传入的对象。
1 | public int CompareTo(Item other) |
知识点三 通过委托函数进行排序
我们可以通过在排序函数传入委托,也就是函数,来指定排序规则。
1 | public delegate int Comparison<in T>(T x, T y); |
comparison 是一个委托变量,所以,我们需要传入一个委托或者一个函数,用来指定排序规则。
其中,这个函数或委托,是一个有两个 T 类型的参数,返回 int。
1 | //传入一个委托(也可以说是匿名函数) |
总结
系统自带的变量(int,float,double…..) 一般都可以直接 Sort
自定义类 SOrt 有两种方式
- 继承接口 IComparable
- 在 Sort 中传入委托函数
协变逆变 知识点
要理解协变逆变,其实就是要遵循里氏替换原则。
知识点一 什么是协变逆变
协变:和谐的变化,自然的变化;和谐的变化,自然的变化。
所以 子类变父类,比如 string 变成 object,感受是和谐的。
逆变:逆常规的变化,不正常的变化;因为 里氏替换原则 父类可以装子类 但是子类不能装父类。
所以 父类变子类,比如 object 变成 string,感受是不和谐的。
协变和逆变是用来修饰泛型的
协变:out
逆变:in
用于在泛型中 修饰 泛型字母的;只有泛型接口和泛型委托能使用
知识点二 作用
返回值 和 参数
用 out 修饰的泛型 只能作为返回值
1 | delegate T TestOut<out T>(); |
用 in 修饰的泛型 只能作为参数
1 | delegate void TestIn<in T>(T t); |
知识点二 作用(结合里氏替换原则理解)
协变 out:父类总是能被子类替换,允许子类返回值委托赋值给父类返回值委托。
out 是修饰返回值的。
out 修饰返回值类型,可以使得系统帮我们判断out 修饰的这个类型的委托的返回值是否可以存储到父类委托当中。
如果不添加 out 修饰符,是不允许子类返回值委托赋值给父类返回值委托。的。
1 | //这里声明了一个Son委托变量os,委托函数返回Son对象 |
逆变 int:父类总是能被子类替换,允许父类参数委托赋值给子类参数委托
in 是修饰参数的。
in 修饰返回值类型,可以使得系统帮我们判断in 修饰的这个类型的委托的参数是否可以存储到子类委托当中。
如果不添加 in 修饰符,是不允许父类参数委托赋值给子类参数委托的
1 | //这里声明了一个Father委托变量if,参数类型为Father |
总结
协变 out
逆变 in
用来修饰 泛型替代符的 只能修饰接口和委托中的泛型
作用两点
- out 修饰的泛型类型 只能作为返回值类型 in 修饰的泛型类型 只能作为 参数类型
- 遵循里氏替换原则的 用 out 和 in 修饰的 泛型委托 可以相互装载(有父子关系的泛型)
- 协变 父类参数泛型委托装子类参数泛型委托 逆变 子类泛型返回值委托装父类泛型返回值委托
多线程 知识点
多线程篇-线程安全-原子性、可见性、有序性解析 - 知乎 (zhihu.com)
知识点一 了解线程前先了解进程
进程(Process)是计算机中的程序关于某数据集合上的一次运行活动
系统进行资源分配和调度的基本单位,是操作系统结构的基础
说人话:打开一个应用程序就是在操作系统上开启了一个进程
进程之间可以相互独立运行,互不干扰
进程之间也可以相互访问、操作
知识点二 什么是线程
操作系统能够进行运算调度的最小单位。
它被包含在进程之中,是进程中的实际运作单位
一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程
我们目前写的程序 都在主线程中
简单理解线程:就是代码从上到下运行的一条“管道”
知识点三 什么是多线程
我们可以通过代码 开启新的线程
可以同时运行代码的多条“管道” 就叫多线程
知识点四 语法相关
线程类 Thread
需要引用命名空间 using System.Threading;
1.声明一个新的线程
注意:线程执行的代码 需要封装到一个函数中
新线程 将要执行的代码逻辑 被封装到了一个函数语句块中
1 | Thread t = new Thread(NewThreadLogic); //这里的参数是一个函数 |
2.启动线程
1 | t.Start(); |
3.设置为后台线程
当前台线程都结束了的时候,整个程序也就结束了,即使还有后台线程正在运行
后台线程不会防止应用程序的进程被终止掉
如果不设置为后台线程 可能导致进程无法正常关闭
1 | t.IsBackground = true; |
4.关闭释放一个线程
如果开启的线程中不是死循环 是能够结束的逻辑 那么 不用刻意的去关闭它
如果是死循环 想要中止这个线程 有两种方式
4.1-死循环中 bool 标识
定义一个全局标记(isRuning),在主线程控制线程运行。
1 | static void NewThreadLogic() |
4.2-通过线程提供的方法
Thread.Abort 方法 (System.Threading) | Microsoft Learn
注意:在.Net core 版本中无法中止 会报错
此方法已过时。 在 .NET 5 及更高版本上,调用此方法会生成编译时警告。 此方法在 .NET 5 及更高版本和 .NET Core 的运行时引发 PlatformNotSupportedException 。
1 | //终止线程 |
5.线程休眠
让线程休眠多少毫秒 1s = 1000 毫秒
在哪个线程里执行 就休眠哪个线程
1 | Thread.Sleep(1000); |
知识点五 线程之间共享数据
多个线程使用的内存是共享的,都属于该应用程序(进程)
所以要注意 当多线程 同时操作同一片内存区域时可能会出问题
可以通过加锁的形式避免问题
lock:对于对象,加互斥锁。
当我们在多个线程当中想要访问同样的东西 进行逻辑处理时
为了避免不必要的逻辑顺序执行的差错
1 | lock(引用类型对象) |
1 | while (true) |
知识点六 多线程对于我们的意义
可以用多线程专门处理一些复杂耗时的逻辑
比如 寻路、网络通信等等
总结
多线程是多个可以同时执行代码逻辑的“管道”
可以通过代码开启多线程,用多线程处理一些复杂的可能影响主线程流畅度的逻辑
关键字 Thread
预处理器指令 知识点
知识点一 什么是编译器
编译器是一种翻译程序
它用于将源语言程序翻译为目标语言程序
源语言程序:某种程序设计语言写成的,比如 C#、C、C++、Java 等语言写的程序
目标语言程序:二进制数表示的伪机器代码写的程序
知识点二 什么是预处理器指令
预处理器指令 指导编译器 在实际编译开始之前对信息进行预处理
预处理器指令 都是以#开始
预处理器指令不是语句,所以它们不以分号;结束
目前我们经常用到的 折叠代码块 就是预处理器指令
知识点三 常见的预处理器指令
#define,#undef
#define 定义一个符号,类似一个没有值的变量
#undef 取消 define 定义的符号,让其失效
两者都是写在脚本文件最前面
一般配合 if 指令使用 或配合特性
1 | //定义一个符号 |
#if,#elif,#else,#endif
和 if 语句规则一样,一般配合#define 定义的符号使用
用于告诉编译器进行编译代码的流程控制
1 | //如果发现有Unity4这个符号 那么其中包含的代码 就会被编译器翻译 |
#warning,#error
告诉编译器,是报警告还是报错误,一般还是配合 if 使用
总结
预处理器指令
可以让代码还没有编译之前就可以进行一些预处理判断
在 Unity 中会用来进行一些平台或者版本的判断
决定不同的版本或者不同的平台使用不同的代码逻辑
反射概念和关键类 Type 知识点
反射是 Unity 工作的基本原理之一
知识点一 什么是程序集
程序集是经由编译器编译得到的,供进一步编译执行的那个中间产物
在 WINDOWS 系统中,它一般表现为后缀为·dll(库文件)或者是·exe(可执行文件)的格式
说人话:
程序集就是我们写的一个代码集合,我们现在写的所有代码
最终都会被编译器翻译为一个程序集供别人使用
比如一个代码库文件(dll)或者一个可执行文件(exe)
知识点二 元数据
元数据就是用来描述数据的数据
这个概念不仅仅用于程序上,在别的领域也有元数据
说人话:
程序中的类,类中的函数、变量等等信息就是 程序的 元数据
有关程序以及类型的数据被称为 元数据,它们保存在程序集中
知识点三 反射的概念
程序正在运行时,可以查看其它程序集或者自身的元数据。
一个运行的程序查看本身或者其它程序的元数据的行为就叫做反射
说人话:
在程序运行时,通过反射可以得到其它程序集或者自己程序集代码的各种信息
类,函数,变量,对象等等,实例化它们,执行它们,操作它们
知识点四 反射的作用
因为反射可以在程序编译后获得信息,所以它提高了程序的拓展性和灵活性
- 程序运行时得到所有元数据,包括元数据的特性
- 程序运行时,实例化对象,操作对象
- 程序运行时创建新对象,用这些对象执行任务
知识点五 语法相关
Type
1 | Type(类的信息类) |
它是反射功能的基础!
它是访问元数据的主要方式。
使用 Type 的成员获取有关类型声明的信息
有关类型的成员(如构造函数、方法、字段、属性和类的事件)
获取 Type
注意:对于同一个类型,获取的 Type(类型)是唯一的!
也就是说,无论你通过什么方式获取 Type,获取到的 Type 变量都是同一个。
1.物之父 object 中的 **GetType()**可以获取对象的 Type
1 | int a = 42; |
2.通过typeof(关键字) 传入类名 也可以得到对象的 Type
1 | Type type2 = typeof(int); |
3.通过类的名字 也可以获取类型
注意:类名必须包含命名空间 不然找不到
1 | Type type3 = Type.GetType("System.Int32"); |
获取程序集
type.Assembly
1 | //可以通过Type可以得到类型所在程序集信息 |
获取类中的所有公共成员
1 | //获取类中的所有公共成员 |
1 | MemberInfo[] infos = t.GetMembers(); |
需要引用命名空间 using System.Reflection;
1 | //首先得到Type |
获取类的公共构造函数并调用
1.获取所有构造函数
1 | ConstructorInfo[] ctors = t.GetConstructors(); |
2.获取其中一个构造函数 并执行
1 | //获取构造函数,types表示参数 |
2-1 得到无参构造
1 | //得构造函数传入 Type数组 数组中内容按顺序是参数类型 |
注意,此时调用的 Invoke,返回一个 Object,所以需要 as。
2-2 得到有参构造
1 | ConstructorInfo info2 = t.GetConstructor(new Type[] { typeof(int) }); |
获取类的公共成员变量
1.得到所有成员变量
1 | FieldInfo[] fieldInfos = t.GetFields(); |
2.得到指定名称的公共成员变量
1 | FieldInfo infoJ = t.GetField("j"); |
3.通过反射获取和设置对象的值
1 | Test test = new Test(); |
3-1 通过反射 获取对象的某个变量的值
1 | Console.WriteLine(infoJ.GetValue(test)); |
3-2 通过反射 设置指定对象的某个变量的值
1 | infoJ.SetValue(test, 100); |
获取类的公共成员方法
通过 Type 类中的 GetMethod 方法 得到类中的方法
MethodInfo 是方法的反射信息
1 | Type strType = typeof(string); |
1.如果存在方法重载 用 Type 数组表示参数类型
1 | MethodInfo subStr = strType.GetMethod("Substring", |
2.调用该方法
注意:如果是静态方法 Invoke 中的第一个参数传 null 即可
1 | string str = "Hello,World!"; |
其它
1 | //Type; |
关键类 Assembly 和 Activator 知识点
Activator
用于快速实例化对象的类
用于将 Type 对象快捷实例化为对象
先得到 Type
然后 快速实例化一个对象
1 | //1.无参构造 |
主要是通过 CreateInstance 方法,快捷创建一个对象的实例。
1 | public static object? CreateInstance(Type type, params object?[]? args); |
Assembly
程序集类
主要用来加载其它程序集,加载后,才能用 Type 来使用其它程序集中的信息。
如果想要使用不是自己程序集中的内容 需要先加载程序集,比如 dll 文件(库文件)。
dll:简单的把库文件看成一种代码仓库,它提供给使用者一些可以直接拿来用的变量、函数或类
三种加载程序集的函数
一般用来加载在同一文件下的其它程序集
1 | Assembly asembly2 = Assembly.Load("程序集名称"); |
一般用来加载不在同一文件下的其它程序集
1 | Assembly asembly = Assembly.LoadFrom("包含程序集清单的文件的名称或路径"); |
加载过程
1.先加载一个指定程序集
1 | Assembly asembly = Assembly.LoadFrom(@"C:\Users\MECHREVO\Desktop\CSharp进阶教学\Lesson18_练习题\bin\Debug\netcoreapp3.1\Lesson18_练习题"); |
2.在加载程序集中的一个类对象 之后才能使用反射
1 | Type icon = asembly.GetType("Lesson18_练习题.Icon"); |
通过反射 实例化一个 icon 对象
1 | Type moveDir = asembly.GetType("Lesson18_练习题.E_MoveDir"); |
直接实例化对象
1 | object iconObj = Activator.CreateInstance(icon, 10, 5, right.GetValue(null)); |
得到对象中的方法 通过反射
1 | MethodInfo move = icon.GetMethod("Move"); |
类库工程创建
我们可以自己创建类库,编译后,将生成 dll 文件。
我们可以直接引用 dll 文件,或者通过反射获取。
特性 知识点
知识点一 特性是什么
特性是一种允许我们向程序的程序集添加元数据的语言结构
它是用于保存程序结构信息的某种特殊类型的类
特性提供功能强大的方法以将声明信息与 C# 代码(类型、方法、属性等)相关联。
特性与程序实体关联后,即可在运行时使用反射查询特性信息
特性的目的是告诉编译器把程序结构的某组元数据嵌入程序集中
它可以放置在几乎所有的声明中(类、变量、函数等等申明)
说人话:特性本质是个类,我们可以利用特性类为元数据添加额外信息
比如一个类、成员变量、成员方法等等为他们添加更多的额外信息
之后可以通过反射来获取这些额外信息
知识点二 自定义特性
特性实质就是一个类,所以,只需要再声明类时继承 Attribute 基类,那么这个类就是一个特性。
特性命名规范:特性名 Attribute
1 | [ ] |
知识点三 特性的使用
基本语法
1 | //[特性名(参数列表)] |
写在哪里?
类、函数、变量上一行,表示他们具有该特性信息
1 | [ ] |
判断是否使用了某个特性
参数一:特性的类型
参数二:代表是否搜索继承链(属性和事件忽略此参数)
1 | if( t.IsDefined(typeof(MyCustomAttribute), false) ) |
获取 Type 元数据中的所有特性
1 | object[] array = t.GetCustomAttributes(true); |
知识点四 限制自定义特性的使用范围
通过为特性类 加特性 限制其使用范围
1 | [ ] |
参数一:AttributeTargets —— 特性能够用在哪些地方
参数二:AllowMultiple —— 是否允许多个特性实例用在同一个目标上
参数三:Inherited —— 特性是否能被派生类和重写成员继承
知识点五 系统自带特性——过时特性
过时特性:Obsolete
用于提示用户 使用的方法等成员已经过时 建议使用新方法
一般加在函数前的特性
1 | public ObsoleteAttribute(string? message, bool error); |
1 | //参数一:调用过时方法时 提示的内容 |
知识点六 系统自带特性——调用者信息特性
需要引用命名空间 using System.Runtime.CompilerServices;
哪个文件调用?
CallerFilePath 特性
哪一行调用?
CallerLineNumber 特性
哪个函数调用?
CallerMemberName 特性
一般作为函数参数的特性
1 | public void SpeakCaller(string str, [CallerFilePath]string fileName = "", |
知识点七 系统自带特性——条件编译特性
条件编译特性:Conditional
它会和预处理指令 #define 配合使用
需要引用命名空间 using System.Diagnostics;
主要可以用在一些调试代码上
有时想执行有时不想执行的代码
只有 define 了 conditionString,被特性修饰的东西才能执行
1 | public ConditionalAttribute(string conditionString); |
知识点八 系统自带特性——外部 Dll 包函数特性
DllImport
用来标记非.Net(C#)的函数,表明该函数在一个外部的 DLL 中定义。
一般用来调用 C 或者 C++的 Dll 包写好的方法
需要引用命名空间 using System.Runtime.InteropServices
1 | public DllImportAttribute(string dllName); |
总结
特性是用于 为元数据再添加更多的额外信息(变量、方法等等)
我们可以通过反射获取这些额外的数据 来进行一些特殊的处理
自定义特性——继承Attribute类
系统自带特性:过时特性等
为什么要学习特性
Unity 引擎中很多地方都用到了特性来进行一些特殊处理
迭代器 知识点
迭代器一定是可迭代对象。迭代器要求实现 movenext 方法。可迭代对象要求实现 getIEnumerator 方法,getIEnumerator 方法返回的是该对象的迭代器。迭代器不负责维护可迭代对象的数据信息,只负责维护读取数据的位置。所以就是工厂和螺丝钉的关系
知识点一 迭代器是什么
迭代器(iterator)有时又称光标(cursor)
是程序设计的软件设计模式
迭代器模式提供一个方法顺序访问一个聚合对象中的各个元素
而又不暴露其内部的标识
在表现效果上看
是可以在容器对象(例如链表或数组)上遍历访问的接口
设计人员无需关心容器对象的内存分配的实现细节
可以用 foreach 遍历的类,都是实现了迭代器的
知识点二 标准迭代器的实现方法
关键接口:IEnumerator,IEnumerable
命名空间:using System.Collections;
可以通过同时继承 IEnumerable 和 IEnumerator 实现其中的方法
IEnumerator
IEnumerator 就是迭代器接口,拥有三个函数,用来实现遍历操作。
1 | public interface IEnumerator |
IEnumerable
IEnumerable 的作用是获取 IEnumerator,并重置下标。
一般 Reset 函数在 GetEnumerator()中调用。
1 | public interface IEnumerable { |
foreach 本质
1 | foreach (var item in list) { } |
- 先获取 in 后面这个对象的 IEnumerator,会调用对象其中的 GetEnumerator 方法 来获取
- 执行得到这个 IEnumerator 对象中的 MoveNext 方法
- 只要 MoveNext 方法的返回值时 true 就会去得到 Current,然后复制给 item
标准迭代器的实现方法
标准迭代器的实现方法,其实就是实现 IEnumerator,IEnumerable 这两个接口。
注意:下标最开始应该是-1,因为开始遍历时,就会调用一次 MoveNext();
1 | class CustomList : IEnumerable, IEnumerator |
知识点三 用 yield return 语法糖实现迭代器
yield return
yield return 是 C#提供给我们的语法糖
所谓语法糖,也称糖衣语法
主要作用就是将复杂逻辑简单化,可以增加程序的可读性
从而减少程序代码出错的机会
在执行
1 | yield return item; |
之后,程序会返回之前调用的函数,当需要再次获取 item 时,又会回到 yield return 然后返回下一个元素。
用 yield return 语法糖实现迭代器
关键接口:IEnumerable
命名空间:using System.Collections;
让想要通过 foreach 遍历的自定义类只需要实现接口中的方法 GetEnumerator 即可
1 | class CustomList2 : IEnumerable |
知识点四 用 yield return 语法糖为泛型类实现迭代器
1 | class CustomList<T> : IEnumerable |
总结
迭代器就是可以让我们在外部直接通过 foreach 遍历对象中元素而不需要了解其结构
主要的两种方式
- 传统方式 继承两个接口 实现里面的方法
- 用语法糖 yield return 去返回内容 只需要继承一个接口即可
特殊语法 知识点
知识点一 var 隐式类型
其实就是 Cpp 的 auto
var 是一种特殊的变量类型
它可以用来表示任意类型的变量
注意:
- var 不能作为类的成员 只能用于临时变量声明时,也就是 一般写在函数语句块中
- var 必须初始化(因为需要类型推导)
1 | var i = 5; |
知识点二 设置对象初始值,大括号初始化法
声明对象时,可以通过直接写大括号的形式初始化公共成员变量和属性
1 | Person p = new Person(100) { sex = true, Age = 18, Name = "唐老狮" }; |
知识点三 设置集合初始值
声明集合对象时,也可以通过大括号 直接初始化内部属性
1 | int[] array2 = new int[] { 1, 2, 3, 4, 5 }; |
知识点四 匿名类型
var 变量可以申明为自定义的匿名类型
1 | var v = new { age = 10, money = 11, name = "小明" }; |
知识点五 可空类型
[C#中 ??、 ?、 ?: 、?.、? ] 问号 - 幽冥狂_七 - 博客园 (cnblogs.com)
1.值类型是不能赋值为 空的
1 | int c = null; //error |
2.申明时 在值类型后面加? 可以赋值为空
1 | int? c = 3; |
3.判断是否为空
1 | if( c.HasValue ) |
4.安全获取可空类型值
4-1.如果为空 默认返回值类型的默认值
1 | Console.WriteLine(value.GetValueOrDefault()); |
4-2.也可以指定一个默认值
1 | Console.WriteLine(value.GetValueOrDefault(100)); |
?.,?[]
先检测对象是否为空,再执行方法
相当于是一种语法糖 能够帮助我们自动去判断 o 是否为空
1 | //如果obj不为空,就执行ToString() |
知识点六 空合并操作符
空合并操作符 ??
语法:左边值 ?? 右边值
语义:如果左边值为 null 就返回右边值 否则返回左边值
只要是可以为 null 的类型都能用
是三目运算符的一种特殊缩略写法。
1 | int? intV = null; |
知识点七 内插字符串
关键符号:$
用$来构造字符串,让字符串中可以拼接变量
1 | string name = "唐老狮"; |
知识点八 单句逻辑简略写法
当循环或者 if 语句中只有 一句代码时 大括号可以省略
1 | if (true) |
值和引用类型补充
知识回顾
值类型
无符号:byte,ushort,uint,ulong
有符号:sbyte,short,int,long
浮点数:float,double,decimal
特殊:char,bool
枚举:enum
结构体 :struct
引用类型:string,数组,class,interface,委托
值类型和引用类型的本质区别:值的具体内容存在栈内存上;引用的具体内容存在堆内存上
问题一 如何判断 值类型和引用类型
F12 进到类型的内部去查看
是 class 就是引用
是 struct 就是值
问题二 语句块
语句块一共有以下几种:
命名空间
↓
类、接口、结构体
↓
函数、属性、索引器、运算符重载等(类、接口、结构体)
↓
条件分支、循环
上层语句块:类、结构体
中层语句块:函数
底层的语句块: 条件分支 循环等
我们的逻辑代码写在哪里?
函数、条件分支、循环 - 中底层语句块中
我们的变量可以申明在哪里?
上、中、底都能申明变量
上层语句块中:成员变量
中、底层语句块中:临时变量
问题三 变量的生命周期
编程时大部分都是 临时变量,在中底层申明的临时变量(函数、条件分支、循环语句块等)
语句块执行结束,没有被记录的对象将被回收或变成垃圾
值类型:被系统自动回收
引用类型:栈上用于存地址的房间被系统自动回收,堆中具体内容变成垃圾,待下次 GC 回收
想要不被回收或者不变垃圾,则必须将其记录下来
如何记录?
在更高层级记录或者,使用静态全局变量记录
问题四 结构体中的值和引用
结构体本身是值类型
前提:该结构体没有做为其它类的成员
在结构体中的值,栈中存储值具体的内容
在结构体中的引用,堆中存储引用具体的内容
引用类型始终存储在堆中,真正通过结构体使用其中引用类型时只是顺藤摸瓜
问题五 类中的值和引用
类本身是引用类型
在类中的值,堆中存储具体的值(托管值类型)
在类中的引用,堆中存储具体的值
值类型跟着大哥走,引用类型一根筋
问题六 数组中的存储规则
数组本身是引用类型
值类型数组,堆中房间存具体内容
引用类型数组,堆中房间存地址
问题七 结构体继承接口
利用里氏替换原则,用接口容器装载结构体存在装箱拆箱
1 | TestStruct obj1 = new TestStruct(); |
插入排序
知识点一 插入排序的基本原理
8 7 1 5 4 2 6 3 9
两个区域
排序区
未排序区
用一个索引值做分水岭
未排序区元素
与排序区元素比较
插入到合适位置
直到未排序区清空
知识点二 代码实现
1 | //实现升序 把 大的 放在最后面 |
1 | int[] nums = new int[] { 3, 5, 6, 9, 0, 1, 2, 4, 6, 8 }; |
知识点三 总结
为什么有两层循环?
第一层循环:一次取出未排序区的元素进行排序
第二层循环:找到想要插入的位置
为什么第一层循环从 1 开始遍历?
插入排序的关键是分两个区域,已排序区 和 未排序区,默认第一个元素在已排序区
为什么使用 while 循环?
满足条件才比较
否则证明插入位置已确定
不需要继续循环
为什么可以直接往后移位置
每轮未排序数已记录
最后一个位置不怕丢
为什么确定位置后,是放在 sortIndex + 1 的位置
当循环停止时,插入位置应该是停止循环的索引加 1 处
基本原理
两个区域
用索引值来区分
未排序区与排序区
元素不停比较
找到合适位置
插入当前元素
套路写法
两层循环
一层获取未排序区元素
一层找到合适插入位置
注意事项
默认(假设)开头已排序,其实一个元素,就是有序的。
第二层循环外插入
希尔排序
知识点一 希尔排序的基本原理
希尔排序是,插入排序的升级版,必须先掌握插入排序
希尔排序的原理:将整个待排序序列,分割成为若干子序列,分别进行插入排序
总而言之:
希尔排序对插入排序的升级主要就是加入了一个步长的概念,通过步长每次可以把原序列分为多个子序列;对子序列进行插入排序,在极限情况下可以有效降低普通插入排序的时间复杂度,提升算法效率。
知识点二 代码实现
1 | int[] arr = new int[] { 8, 7, 1, 5, 4, 2, 6, 3, 9 }; |
1 | int[] nums = new int[] { 3, 5, 6, 9, 0, 1, 2, 4, 6, 8 }; |
知识点三 总结
基本原理
设置步长
步长不停缩小
到 1 排序后结束
具体排序方式
插入排序原理
套路写法
三层循环
一层获取步长
一层获取未排序区元素
一层找到合适位置插入
注意事项
步长确定后
会将所有子序列进行插入排序
归并排序
知识点一 归并排序基本原理
归并 = 递归 + 合并
数组分左右
左右元素相比较
满足条件放入新数组
一侧用完放对面
递归不停分
分完再排序
排序结束往上走
边走边合并
走到头顶出结果
归并排序分成两部分 1.基本排序规则 2.递归平分数组
递归平分数组:
不停进行分割
长度小于 2 停止
开始比较
一层一层向上比
基本排序规则:
左右元素进行比较
依次放入新数组中
一侧没有了另一侧直接放入新数组
知识点二 代码实现
唐老狮做法
1 | //第一步: |
更简洁的做法
1 | public static void MergeSort(int[] nums, int left, int right) |
知识点三 总结
理解递归逻辑
一开始不会执行 Sort 函数的
要先找到最小容量数组时
才会回头递归调用 Sort 进行排序
基本原理
归并 = 递归 + 合并
数组分左右
左右元素相比较
一侧用完放对面
不停放入新数组
递归不停分
分完再排序
排序结束往上走
边走边合并
走到头顶出结果
套路写法
两个函数
一个基本排序规则
一个递归平分数组
注意事项
排序规则函数 在 平分数组函数
内部 return 调用
快速排序
知识点一 快速排序基本原理
选取基准
产生左右标识
左右比基准
满足则换位
排完一次
基准定位
左右递归
直到有序
知识点二 代码实现
快速排序是不稳定排序
为什么快速排序是不稳定的d4shman 的博客-CSDN 博客快排是稳定排序吗
唐老狮写法
有 bug,数组有相同元素的情况下,会死循环。
在查找时加<=解决
1 | //第一步: |
优化写法
1 | public static void QuickSort(int[] nums) |
堆排序
知识点一 堆排序基本原理
构建二叉树,大堆顶调整,堆顶往后方,不停变堆顶
关键规则:最大非叶子节点是 数组长度 / 2 - 1
父节点和叶子节点:父节点为 i,左节点 2i+1,右节点 2i+2
知识点二 代码实现
堆排序的精髓在于,利用数组长度这个参数,每次对其他元素进行 HeapCompare
使得每次排序,忽略最大/最小的元素,已达到排序的效果
唐老狮写法
1 | //第一步:实现父节点和左右节点比较 |
重写
1 | public static void HeapCompare(int[] nums, int nowIndex, int len) |
知识点三 总结
基本原理
构建二叉树
大堆顶调整
堆顶往后方
不停变堆顶
套路写法
3 个函数
1 个堆顶比较
1 个构建大堆顶
1 个堆排序
重要规则
最大非叶子节点索引:数组长度/2 - 1
父节点和叶子节点索引:父节点为 i,左节点 2i+1,右节点 2i-1
注意:
堆是一类特殊的树,堆的通用特点就是父节点会大于或小于所有子节点;
我们并没有真正的把数组变成堆,只是利用了堆的特点来解决排序问题