数据结构学习笔记.md
数据结构学习笔记
概述
数据是外部世界信息的计算机化,是计算机加工处理的对象。
计算机处理数据时必须解决的四个问题:
- 如何在计算机中方便、高效地表示和组织数据
- 如何在计算机存储器(内存和外存)中存储数据
- 如何对存储在计算机中的数据进行操作,可以有哪些操作,如何实现这些操作以及如何对同一问题的不同操作方法进行评价。
- 必须理解每种数据结构的性能特征,以便选择一个适合于某个特定问题的数据结构。
基本概念和术语
-
数据(Data)
数据是外部世界信息的载体,它能够被计算机识别、存储和加工,是计算机程序加工的元才来。它可以是数值数据,例如整数、实数或复数;也可以是非数值类型,例如字符、图形、图像、声音等。
-
数据元素(Data Element)和数据想(Data Item)
数据元素是数据的基本单位,在计算机程序中通常被作为一个整体进行考虑和处理。数据元素有事也被称为元素、结点、顶点、记录等。一个数据元素可以由若干个数据项(Data Item)组成。(可以理解为对象)
数据项是不可分割的、含有独立意义的最小数据单位,数据项有事也称为字段(Field)或域(Domain)。
-
数据对象(Data Object)
数据对象是性质相同的数据元素的集合,是数据的一个子集。(可以理解其为数组)
-
数据类型(Data Type)
数据类型是高级程序设计语言中的概念,是数据的取值范围和对数据进行操作的总和。
数据类型可以分为两类:
- 非结构的原子类型(值类型)
- 结构类型(引用类型)
-
数据结构(Data Structure)
数据结构是相互之间存在一种或多种特定关系的数据元素的集合。在任何问题中,数据元素之间都不是孤立的,而是存在着一定的关系,这种关系称为结构(Structure)。根据数据元素之间关系的不同特征,可以分为4中数据结构
- 集合(Set):该结构中的数据元素除了存在“同属于一个集合”的关系外,不存在任何其他关系
- 线性结构(Linear Structure):该结构中的元素存在着一对一关系
- 树状结构(Tree Structure):该结构中的元素存在着一对多的关系
- 图状机构(Graphic Structure):该结构中的元素存在多对多关系
算法(Algorithm)
算法是对某一特定类型的问题的求解步骤的一种描述,是指令的有限序列。其中的每条指令表示一个或多个操作。
算法的5个特性:
- 有穷性(Finity):一个算法总是在执行有穷步之后结算,即算法的执行时间是有限的。
- 确定性(Unambiguousness):算法的每一个步骤都必须有确切的含义,即无二意,并且对于相同的输入只能有相同的输出。
- 输入(Input):一个算法具有零个或多个输入。它即是在算法开始之前给出的量。这些输入是某数据结构中的数据对象。
- 输出(Output):一个算法具有一个或多个输出,并且这些输出与输入之间存在某种特定的关系。
- 能行性(Realizability):算法中的每一步都可以通过已经实现的基本运算的有限次运行来实现。
算法与程序的区别:
- 程序不一定要满足有穷性(例如操作系统,他会一直运行下去)。
- 程序必须用计算机语言来描述,而算法不一定用计算机语言来描述,可以用自然语言、框图、伪代码描述。
算法的评价标准
-
正确性(Correctness)
算法的执行结果应当满足预先规定的功能和性能的要求,这是评价一个算法的最重要也是最基本的标准。算法的正确性还包括对于输入、输出处理的明确而无歧义的描述。
-
可读性(Readability)
一个算法应当思路清晰、层次分明、简单明了、易读易懂.
-
健壮性(Robustness)
一个算法应该具有很强的容错能力,当输入不合法的数据时,算法应当能做适当的处理,使得不至于引起严重的后果。需要尽可能的考虑到所有的便捷情况和异常情况。
-
运行时间(Running Time)
运行时间是指算法在计算机上运行所花费的时间,它等于算法中每条语句执行时间的总和。
-
占用空间(Storage Space)
占用空间是指算法在计算机上存储所占用的存储空间,包括存储算法本身所占用的存储空间、算法的输入及输出数据所占用的存储空间和算法在运行过程中临时占用的存储空间。
通常把算法在运行过程中临时占用的存储空间的大小叫算法的空间复杂度(Space Complexity)。
算法的空间复杂度比较容易计算,它主要包括局部变量所占用的存储空间和系统为实现递归所使用的堆栈占用的存储空间。
算法的时间复杂度
算法的**时间复杂度(Time Complexity)**是指该算法的运行时间与问题规模的对应关系。
一个算法是由控制结构和原操作构成的,其执行的时间取决于二者的综合效果。为了便于比较同一问题的不同算法,通常把算法中基本操作重复执行的次数(频度)作为算法的时间复杂度。算法中的基本操作一般是指算法中最深层循环内的语句,因此,算法中基本操作语句的频度是问题规模n的某个函数f(n),记作:T(n)=O(f(n))
。其中“O”表示随问题规模n的增大,算法执行时间的增长率和f(n)的增长率相同,或者说,用“O”符号表示数量级的概念。例如,如T(n)=n(n-1)
,则n(n-1)
的数量级与你n^2
相同,所以T(n)=O(n^2)
。
如果一个算法没有循环语句,则算法中基本操作的执行频度与问题规模n无关,记作O(1)
,也称为常数阶。如果算法只有一个一重循环,则算法的基本操作的执行频度与问题规模n呈线性增大关系,记作O(n)
,也叫线性阶。常用的还有平方阶O(n^2)
、立方阶O(n^3)
、对数阶O(log2n)
等。
下面举例来说明计算算法时间复杂度的方法。
/* n>1 */ int x=n,y=0; while(y<x>){ y+=1; } /** 只有一层循环,while循环次数为n,所以最深层内语句的频度是n 则该程序的时间复杂度是T(n)=0(n) **/
/* n>1 */ for(int i=0;i<n;i++){ for(int j=0;j<n;j++){ A[i][j]=i*j; } } /** 只有两层循环,外层循环执行n次,内层循环执行n次,所以最深层内语句的频度是n^2 则该程序的时间复杂度是T(n)=0(n^2) **/
/* n>1 */ while(x>=(y+1)*(y+1)){ y+=1; } /** 只有一层循环,while循环次数为根号下n,所以最深层内语句的频度是根号下n 则该程序的时间复杂度是T(n)=0(根号下n) **/
/* n>1 */ for(int i=0;i<m;i++){ for(int j=0;j<t;j++){ for(int k=0;j<n;k++){ c[i][j]=c[i][j]+a[i][k]*b[k][j]; } } } /** 只有三层循环,外层循环执行m次,中层循环执行t次,内层循环n次,所以最深层内语句的频度是m*t*n 则该程序的时间复杂度是T(n)=0(m*t*n) **/
常用数学属于
计量单位(Unit): 按照IEEE规定的表示法标准,字节缩写为“B”,位缩写为“b”,兆字节(220字节)缩写为缩写为“MB”,千字节(210字节)缩写为“KB”。
阶乘函数(Factorial Function): 阶乘函数n!是指从1到n之间所有整数的连乘,其中n为大于0的整数。因此,5!=1·2·3·4·5=120。特别地,0!=1。
线性表
线性表的逻辑结构
线性表的定义
线性表(List)是由n(n≥0)个相同类型的数据元素构成的有限序列。
对于这个定义应该注意两个概念:
- 是“有限”,指的是线性表中的数据元素的个数是有限的,线性表中的每一个数据元素都有自己的位置(Position)。
- 是“相同类型”,指的是线性表中的数据元素都属于同一种类型。
线性表通常记为:L=(a1,a2,…,ai-1,ai, ai+1,…,an),n=0时的线性表被称为空表(Empty List)。
将ai-1称为ai的直接前驱,将ai称为ai+1的直接后继。除a1外,其余元素只有一个直接前驱;除an外,其余元素只有一个直接后继.
线性表的基本操作
public interface IListDS<T> { int GetLenght(); //求长度 void Clear(); //清空操作 bool IsEmpty(); //判断线性表是否为空 void Append(T item); //附加操作 void Insert(T item, int i); //插入操作 T Delete(int i); //删除操作 T GetElem(int i); //取表元 int IndexOf(T value); //按值查找 }
顺序表
在计算机内,保存线性表最简单、最自然的方式,就是把表中的元素一个接一个地放进顺序的存储单元,这就是线性表的顺序存储(Sequence Storage)。
顺序表的实现
public class SeqList<T>: IListDS<T>{ private int maxsize; //顺序表的容量 private T[] data; // 数组,用于存储顺序表中的数据元素 private int last; //指示顺序表最后一个元素的位置 /// 索引器 public T this[int index]{ get{return data[index];} set{data[index]=value;} } // 最后一个数据元素未知属性 public int Last{ get{return last;} } // 容量属性 public int Maxsize{ get{return maxsize;} set{maxsize=value;} } // 构造函数 public SeqList(int size){ data = new T[size]; maxsize = size; last = -1; } // 求顺序表的长度 public int GetLenght(){ return last+1; } // 清空顺序表 public void Clear(){ last=-1; } // 判断顺序表是否为空 public bool IsEmpty(){ return last == -1; } //判断顺序表是否已满 public bool IsFull(){ return last == maxsize-1; } //在顺序表的末尾添加新元素 public void Append(T item){ if(IsFull()){ throw new Exception("List is full"); } data[++last]=item; } // 在顺序表的第i个数据元素的位置插入一个数据元素 // i 从1开始计数 public void Insert(T item, int i){ if(IsFull()){ throw new Exception("List is full"); } if(i<1||i>last+2){ throw new Exception("Position is error"); } if(i == last+2){ data[last+1]=item; } else{ for(int index=last;index>=i-1;index--){ data[index+1]=data[index]; } data[i-1]=item; } last++; } // 删除顺序表中的第i个数据元素 // i从1开始计数 public T Delete(int i){ T tmp = default(T); if(IsEmpty()){ throw new Exception("List is empty"); } if(i<1||i>last+1){ throw new Exception("Position is error"); } if(i == last+1){ tmp=data[last]; } else{ temp=data[i-1]; for(int j = i-1;j<=last;j++){ data[j]=data[j+1]; } } last--; return tmp; } // 获得顺序表的第i个数据元素 public T GetElem(int i) { if (IsEmpty() || (i<1) || (i>last+1)) { throw new Exception("List is empty or Position is error!"); } return data[i-1]; } // 获取值value在顺序表中的下标 public int IndexOf(T value){ if(IsEmpty()){ return -1; } int i=0; for(i,i<=last;i++){ if(value.Equals(data[i])){ break; } } if(i>last){ return -1; } return i; } // 颠倒顺序表 public void Revers(){ if(IsEmpty()){ return; } int len = GetLenght(); for(int i=0;i<len/2;i++){ T tmp = data[len-i]; data[len-i]=data[i]; data[i]=tmp; } } }
连接两个顺序表
有数据类型为整型的顺序表La和Lb,其数据元素均按从小到大的升序排列,编写一个算法将它们合并成一个表Lc,要求Lc中数据元素也按升序排列。
算法思路
依次扫描La和Lb的数据元素,比较La和Lb当前数据元素的值,将较小值的数据元素赋给Lc,如此直到一个顺序表被扫描完,然后将未完的那个顺序表中余下的数据元素赋给Lc即可。Lc的容量要能够容纳La和Lb两个表相加的长度。
public SeqList<T> Merge<T>(SeqList<T> la,SeqList<T> lb){ SeqList<T> nl = new SeqList<T>(la.Maxsize+lb.Maxsize); int aLen = la.GetLenght(); int bLen = lb.GetLenght(); int index=0,bIndex=0; for(int i=0;i<aLen;i++){ if(bIndex==bLen){ // 如果lb被扫描完了,则将la剩下的元素直接加入nl中 nl.Append(al[i]); } else{ whil(bIndex<bLen){ if(la[i]>lb[bIndex]){ // 如果lb中元素小,则将lb元素加入nl中,并继续循环lb元素 nl.Append(lb[bIndex++]); }else if(la[i]<lb[bIndex]){ // 如果la中元素小,则将la元素加入nl中,并跳出lb循环 nl.Append(al[i]); break; }else{ // 如果相等,则将la元素和lb元素加入nl中,并跳出lb循环 nl.Append(al[i]); nl.Append(lb[bIndex++]); break; } } } } while(bIndex<bLen){ // 如果lb没有被扫描完,则lb剩下的元素直接加入nl中 nl.Append(al[bIndex++]); } return nl; }
public SeqList<T> Merge<T>(SeqList<T> la,SeqList<T> lb){ SeqList<T> nl = new SeqList<T>(la.Maxsize+lb.Maxsize); int i =0; int j = 0; // 两表都有数据 while(i<la.GetLenght()&&j<lb.GetLenght()){ if(la[i]<lb[j]){ nl.Append(la[i++]); } else{ nl.Append(lb[j++]); } } // a表有数据 while(i<la.GetLenght()){ nl.Append(la[i++]); } // b表有数据 while(j<lb.GetLenght()){ nl.Append(lb[j++]); } return nl; }
时间复杂度为
T(n)=0(m+n)
,m
为la表的长度,n
是lb表的长度。
去除顺序表中重复值
去除la表中重复值,并返回新的顺序表
思路
将la表中的第一个元素给新表lb。然后从表la的第二个元素开始循环,与表lb中的值做比较,如果值不相同就添加进表lb,最后返回表lb。
public SeqList<T> Purge<T>(SeqList<T> la){ SeqList<T> lb = new SeqList<T>(la.Maxsize); if(la.IsEmpty()) return lb; lb.Append(la[0]); for(int i=1;i<la.GetLenght();i++){ int j=0; // 查看lb表中是否有相同值 for(j=0;j<lb.GetLenght();j++){ // 有相同值,跳出内部循环 if(la[i]==lb[j]) break; } // 在lb表中没有找到相同的值,则将值添加进lb表中 if(j>lb.GetLenght()) lb.Append(la[i]); } return lb; }
算法的时间复杂度为
T(n)=0(m!+(n-m)m)
,m
为la表的长度,n
是lb表的长度。
单链表
线性表的另外一种存储结构——链式存储(Linked Storage),这样的线性表叫链表(Linked List)。链表不要求逻辑上相邻的数据元素在物理存储位置上也相邻,因此,在对链表进行插入和删除时不需要移动数据元素,但同时也失去了顺序表可随机存储的优点。
顺序表的优缺点
优点:顺序表是用地址连续的存储单元顺序存储线性表中的各个数据元素,逻辑上相邻的数据元素在物理位置上也相邻。因此,在顺序表中查找任何一个位置上的数据元素非常方便。
缺点:在对顺序表进行插入和删除时,需要通过移动数据元素来实现,影响了运行效率。
链表的优缺点
链表不要求逻辑上相邻的数据元素在物理存储位置上也相邻
优点:在对链表进行插入和删除时不需要移动数据元素
缺点:失去了顺序表可随机存储的优点。
单链表的定义
链表是用一组任意的存储单元来存储线性表中的数据元素(这组存储单元可以是连续的,也可以是不连续的),在存储数据元素时,除了存储数据元素本身的信息外,还要存储与它相邻的数据元素的存储地址信息。
这两部分信息组成该数据元素的存储映像(Image),称为结点(Node)。把存储据元素本身信息的域叫结点的数据域(Data Domain),把存储与它相邻的数据元素的存储地址信息的域叫结点的引用域(Reference Domain)。因此,线性表通过每个结点的引用域形成了一根“链条”,这就是“链表”名称的由来。
如果结点的引用域只存储该结点直接后继结点的存储地址,则该链表叫单链表(Singly Linked List)。把该引用域叫next。
单链表结点的的类实现:
public class Node<T>{ private T data; private Node<T> next; public Node(){ data=default(T); next=null; } public node(T data){ this.data=data; next=null; } public node(T data,Node<T> next){ this.data = data; this.next = next; } public T Data{ get{return data;} set{data=value;} } public Node<T> Next{ get{return next;} set{next=value;} } }
线性表(a1,a2,a3,a4,a5,a6)对应的链式存储结构示意图:
头引用:867
存储地址 | data | next |
---|---|---|
500 | a4 | 710 |
…… | …… | …… |
560 | a2 | 930 |
…… | …… | …… |
710 | a5 | 855 |
…… | …… | …… |
855 | a6 | null |
…… | …… | …… |
867 | a1 | 560 |
…… | …… | …… |
930 | a3 | 500 |
单链表类LinkList<T>
的实现说明如下所示。
public LinkList<T> : IListDS<T> { private Node<T> head; public Node<T> Head{ get{return head;} set{head=value;} } public LinkList(){ head=null; } public T this[index]{ return GetElem(index+1); } //求长度 public int GetLenght(){ if(head==null) return -1; int lenght=0; Node<T> p=head; while(p!=null){ p=p.Next; lenght++; } return lenght; } // 清空操作 public void Clear(){ head=null; } // 判断线性表是否为空 public bool IsEmpty(){ return head==null; } // 附加操作 public void Append(T item){ Node<T> data = new Node<T>(item); if(head == null){ head = data; } else{ Node<T> p = head; while(p.Next!=null){ p = p.Next; } p.Next = data; } } // 插入操作 // i从1开始计数 public void Insert(T item, int i){ if(i<1) throw new Exception("Position is error"); Node<T> data = new Node<T>(item); if(i==1){ data.Next=head; head=data; return; } Node<T> p = head; int index = 1; // 找到第i个结点的前驱结点 while(index++ < i-1){ if(p==null) throw new Exception("Position is error"); p=p.Next; } if(p.Next != null) data.Next=p.Next.Next; p.Next=data; } //删除操作 // i从1开始计数 public T Delete(int i){ if(i<1) throw new Exception("Position is error"); Node<T> p = head; if(i==1){ head=head.next; return p.Data; } int index = 1; // 找到第i个结点的前驱结点 while(index++ < i-1){ if(p==null) throw new Exception("Position is error"); p=p.Next; } if(p.Next==null) throw new Exception("Position is error"); T data = p.next.Data; p.next=p.next.next; return Data; } //取表元 // i从1开始计数 public T GetElem(int i){ if(IsEmpty()) throw new Exception("List is empty!"); if(i<1) throw new Exception("Position is error"); Node<T> p = head; int index = 1; while(index++ < i){ if(p==null) throw new Exception("Position is error"); p=p.Next; } return p.Data; } //按值查找 public int IndexOf(T value){ if(IsEmpty()) throw new Exception("List is empty!"); int index=0; Node<T> p = head; while(p != null){ if(p.Data == value) return index; p = p.Next; index++; } return -1; } // 倒置单链表 public void Reverse(){ Node<T> p=head.Next; Node<T> q=null; head.next=null; // 循坏源链表,并拼接新链表,将新链表赋值给head while(p!=null){ q = p; // 设置q为当前结点 p = p.Next; // p 指向后继结点 q.Next = head; // 将结点q设置为head的前驱结点 head = q; // 将head设置为q } } }
连接两单链表
有数据类型为整型的单链表Ha和Hb,其数据元素均按从小到大的升序排列,编写一个算法将它们合并成一个表Hc,要求Hc中结点的值也是升序排列。
算法思路
把Ha的头结点作为Hc的头结点,依次扫描Ha和Hb的结点,比较Ha和Hb当前结点数据域的值,将较小值的结点附加到Hc的末尾,如此直到一个单链表被扫描完,然后将未完的那个单链表中余下的结点附加到Hc的末尾即可。
public LinkList<T> Merge<T>(LinkList<T> Ha,LinkList<T> Hb){ LinkList<T> Hc = new LinkList<T>(); Node<T> p = Ha.Head; Node<T> q = Hb.Head; Node<T> s = null; while(p!=null&&q!=null){ if(p.Data<q.Data){ s = p; p = p.Next; }else{ s = q; q = q.Next; } Hc.Append(s.Data); } if(p==null) p=q; while(p!=null){ s = p; p = p.Next; Hc.Append(s.Data); } return Hc; }
该方法是将结点添加到单链的结尾,而单链表每次将结点添加到链表结尾时都需要循环整个链表,以便定位到最后一个结点。。而把结点插入到单链表的头部要节省很多时间,因为这不需要遍历链表。但由于是把结点插入到头部,所以得到的单链表是逆序排列而不是升序排列。
将结点插入到头部的实现如下:
public LinkList<T> Merge<T>(LinkList<T> Ha,LinkList<T> Hb){ LinkList<T> Hc = new LinkList<T>(); Node<T> p = Ha.Head; Node<T> q = Hb.Head; Node<T> s = null; while(p!=null&&q!=null){ if(p.Data<q.Data){ s = p; p = p.Next; }else{ s = q; q = q.Next; } s.Next=Hc.Head; Hc.Head=s; } if(p==null) p=q; while(p!=null){ s = p; p = p.Next; s.Next=Hc.Head; Hc.Head=s; } return Hc; }
去除单链表中重复值
已知一个存储整数的单链表Ha,试构造单链表Hb,要求单链表Hb中只包含单链表Ha中所有值不相同的结点。
算法思路: 先申请一个结点作为Hb的头结点,然后把Ha的第1个结点插入到Hb的头部,然后从Ha的第2个结点起,每一个结点的数据域的值与Hb中的每一个结点的数据域的值进行比较,如果不相同,则把该结点插入到Hb的头部。
实现如下:
public LinkList<T> Purge<T>(LinkList<T> Ha){ LinkList<T> Hb = new LinkList<T>(); Node<T> p = Ha.Head; Node<T> q; Node<T> s; Hb.Head = p; p = p.Next; Hb.Head.Next = null; while(p != null){ s = p; p = p.Next; q = Hb.Head; // 在Hb中查找相同的值 while(q != null && s.Data != q.Data){ q = q.Next; } // 在Hb中没有找到相同的值 if(q == null){ s.Next = Hb.Head; Hb.Head = s; } } return Hb; }
其他链表
双向链表
在单向链表的基础上,在每个结点中添加前驱结点引用。
结点类的实现:
public class DbNode<T>{ private T data; private DbNode<T> prev; private DbNode<T> next; public T Data{ get {return data;} set {data=value;} } public DbNode<T> Prev{ get{return prev;} set{prev=value;} } public DbNode<T> Next{ get{return next;} set{next=value;} } public DbNode(){ data = default(T); } public DbNode(T data){ this.data = data; } public DbNode(DbNode<T> prev,T data){ this.prev = prev; this.data = data; } public DbNode(T data,DbNode<T> next){ this.data = data; this.next = next; } public DbNode(DbNode<T> prev,T data,DbNode<T> next){ this.prev = prev; this.data = data; this.next = next; } }
由于双向链表的结点有两个引用,所以,在双向链表中插入和删除结点比单链表要复杂。双向链表中结点的插入分为在结点之前插入和在结点之后插入,插入操作要对四个引用进行操作。
假设p是指向双向链表的某已结点,在结点p前插入结点s,步骤如下:
- s.Prev = p.Prev
- s.Prev.Next = s
- s.Next = p
- p.Prev = s
在双向链表中删除结单p,步骤如下
- p.Prev.Next = p.Next
- p.Next.Prev = p.Prev
栈和队列
栈:先进后出
队:先进先出
栈的实现
public interfae IStack<T>{ int GetLenght(); // 求栈的长度 bool IsEmpty(); // 判断栈是否为空 void Clear(); // 清空栈 void Push(T item); // 入栈操作 T Pop(); // 出栈操作 T GetTop(); // 取栈顶元素 }
顺序栈
public class SeqStack<T>: IStack<T>{ private int maxsize; private int top; private T data; public int MaxSize{ get{return maxsize;} set{maxsize = value;} } public int Top{ get{return top;} set{value = top;} } public SeqStack(int maxsize){ this.maxsize = maxsize; data = new T[maxsize]; top =-1; } public T this[int index]{ get{return data[index];} set{data[index]=value;} } // 求栈的长度 public int GetLenght(){ return Top+1; } // 判断栈是否为空 public bool IsEmpty(){ return top < 0; } public bool IsFull(){ return top > = maxsize; } // 清空栈 public void Clear(){ top = -1; } // 入栈操作 public void Push(T item){ if(IsFull()) throw new Exception("Stack is full"); data[++top]=item; } // 出栈操作 public T Pop(){ if(IsEmpty()) return default(T); return data[top--]; } // 取栈顶元素 public T GetTop(){ if(IsEmpty()) return default(T); return data[top]; } }
链表栈
public class LinkStack<T>:IStack<T>{ private Node<T> top; private int num; public LinkStack(){ top = null; num = 0; } public LinkStack(Node<T> top){ this.top = top; num=1; Node<T> p = top.Next; while(p!=null){ num++; p = p.Next; } } // 求栈的长度 public int GetLenght(){ return num; } // 判断栈是否为空 public bool IsEmpty(){ return num == 0; } // 清空栈 public void Clear(){ top = null; num = 0; } // 入栈操作 public void Push(T item){ Node<T> p = new Node<T>(item); if(IsEmpty()){ top = p; } else{ p.Next = top; top = p; } num++; } // 出栈操作 public T Pop(){ if(IsEmpty()){ return default(T); } T data = top.Data; top = top.Next; num--; return data; } // 取栈顶元素 public T GetTop(){ if(IsEmpty()){ return default(T); } return top.Data; } }
栈的应用
数制转换问题
数制转换问题是将任意一个非负的十进制数转换为其它进制的数,这是计算机实现计算的基本问题。一般的解决方法是辗转相除法。以将一个十进制数N转换为八进制数为例进行说明。假设N=5142,示例如下
N | N/8(整除) | N%8(求余) | 位序 |
---|---|---|---|
5142 | 642 | 6 | 底 |
642 | 80 | 2 | |
80 | 10 | 0 | |
10 | 1 | 2 | |
1 | 0 | 1 | 高 |
转换得到的八进制数各个数位是按从低位到高位的顺序产生的,而转换结果的输出通常是按照从高位到低位的顺序依次输出。也就是说,输出的顺序与产生的顺序正好相反,这与栈的操作原则相符。所以,在转换过程中可以使用一个栈,每得到一位八进制数将其入栈,转换完毕之后再依次出栈。
算法思想如下:当N>0时,重复步骤1和步骤2。
步骤1:若N≠0,则将N%8压入栈中,执行步骤2;若N=0。则将栈的内容依次出栈,算法结束。
步骤2:用N/8代替N,返回步骤1。
用链栈存储转换得到的数位。
算法实现如下:
// n 为待转换数值 b 为转换的进制 public void Conversion(int n,int b){ LinkStack<int> s = new LinkStack<int>(); if(n == 0) return; while(n>0){ s.Push(n%b); n = n/b } while(!s.IsEmpty()){ Console.WriteLine("{0}", s.Pop()); } }
括号匹配
算法思想
- 如果栈为空,则将括号入栈
- 如果括号与栈顶的括号匹配,则将栈顶括号找出
- 如果括号与栈顶括号不匹配,则将括号入栈
public bool MatchBracket(char[] charlist){ int len = charlist.Lenght; SeqStack<char> s = new SeqStack<char>(len); for(int i = 0;i < len;i++>){ char c = charlist[i]; if(s.IsEmpty()){ s.Push(c) }else{ char top = s.GetTop(); if((top =='(' && c == ')') ||(top == '[' && c == ']') ||(top == '{' && c == '}')){ s.Pop(); }else{ s.Push(c) } } } return s.IsEmpty(); }
表达式求值
“算符优先算法”是用运算符的优先级来确定表达式的运算顺序,从而对表达式进行求值。在机器内部,任何一个表达式都是由操作数(Operand)、运算符(Operator)和界限符(Delimiter)组成。
操作数和***运算符***是表达式的主要部分。
分界符标志了一个表达式的结束。根
据表达式的类型,表达式分为三类,即算术表达式、关系表达式和逻辑表达式。
为简化问题,我们仅讨论四则算术运算表达式,并且假设一个算术表达式中只包含加、减、乘、除、左圆括号和右圆括号等符号,并假设‘#’是界限符。
算术四则运算的规则如下:
- 先乘除后加减;
- 先括号内后括号外;
- 同级别时先左后右。
相邻运算符之间的优先级关系:
左边运算符/右边运算符 | + | - | * | / | ( | ) | # |
---|---|---|---|---|---|---|---|
+ | 大于 | 大于 | 小于 | 小于 | 小于 | 大于 | 大于 |
- | 大于 | 大于 | 小于 | 小于 | 小于 | 大于 | 大于 |
* | 大于 | 大于 | 大于 | 大于 | 大于 | 大于 | 大于 |
/ | 大于 | 大于 | 大于 | 大于 | 大于 | 大于 | 大于 |
( | 小于 | 小于 | 小于 | 小于 | 小于 | 等于 | |
) | 大于 | 大于 | 大于 | 大于 | 大于 | 等于 | |
# | 小于 | 小于 | 小于 | 小于 | 小于 | 等于 |
为实现算法,使用两个栈,一个存放算法(OPTR),一个存放操作数和运算的结果(OPND)
算法思想如下:
- 首先将OPND设置为空,将'#'入OPTR
- 一次读入表达式中的每个字符,若是操作数则将该字符入OPND,若是运算符,则和OPTR栈顶字符比较优先级,若OPTR栈顶字符优先级高,则将OPND栈中的两个操作数和OPTR栈顶运算符出栈,然后将操作结果入OPND;若OPTR栈顶字符优先级低,则将改资费入OPTR;若儿子优先级相等,则OPTR栈顶字符出栈并读入下一个字符。
实现如下:
public decimal EvaluateExpression(string str){ SeqStack<char> optr = new SeqStack <char>(20); SeqStack<decimal> opnd = new SeqStack <decimal>(20); optr.Push('#'); string c = GetOptFile(str); while(c != ""){ if((c != "+") && (c != "-") && (c != "*") && (c != "/") && ( c != "(") && (c != ")")) { optr.Push(decimal.prase(c)); c = GetOptFile(str); } else{ switch(Precede(optr.GetTop(),op)){ case '<': optr.Push(c); c = GetOptFile(str); break; case '>': opnd.Push(Operate(opnd.Pop(),optr.Pop(),opnd.Pop())) optr.Push(op); break; case '=': optr.Pop(); c = GetOptFile(str); break; default: throw new Exception("算术表达式不正确!"); } } } return optr.GetTop(); }
GetOptFile
方法,从字符串中获取下一个计算的数字或运算符
Precede
方法,比较两个运算符优先级
Operate
方法,二元计算
队列的实现
public interface IQueue<T> { int GetLength(); //求队列的长度 bool IsEmpty(); //判断对列是否为空 void Clear(); //清空队列 void In(T item); //入队 T Out(); //出队 T GetFront(); //取对头元素 }
顺序队列
顺序队列(Sequence Queue):用一片连续的存储空间来存储队列中的数据元素。用一维数组来存放顺序队列中的数据元素。队头位置设在数组下标为0的端,用front
表示;队尾位置设在数组的另一端,用rear
表示。front
和rear
随着插入和删除而变化。当队列为空时,front=rear=-1
。
假溢出:当front
不在0的位置,而rear
已到以为数组的最后一个元素,这时再添加新的元素,一位数组就会溢出,而实际上队列中并未满,还有空闲空间,这种现象称为假溢出。例如,队列中一位数组为[a1,a2,a3,a4,a5,a6],front=3
,rear=5
,在队列中 a1,a2,a2为空闲空间。
循环顺序队列(Circular sequence Queue):将队列看作是首尾相连的,当rear=5
时,这时再新增元素,rear
指向数组下标0,从而解决假溢出问题。
队尾指示器
rear
加1操作为:rear=(rear+1)%maxsize
对头指示器
front
加1操作为:front=(front+1)%maxsize
maxsize
为数组最大长度
由于将对量首尾相连,所以在满队和空队时都有front=rear。解决这个问题的一般方法是少用一个空间。所以空队的条件是front==rear
,而满队的条件是(rear+1)/maxsize==front
。 队列元素个数=(realmaxsize-front)/maxsize
。
public class CSeqQueue<T> : IQueue<T> { private int maxsize; //循环顺序队列的容量 private T[] data; //数组,用于存储循环顺序队列中的数据元素 private int front; //指示循环顺序队列的队头 private int rear; //指示循环顺序队列的队尾 public int Front{ get {return front;} private set {front=(value)%maxsize;} } public int Rear{ get {return rear;} private set {rear=(value)%maxsize;} } public int Maxsize{ get{return maxsize;} private set{maxsize=value;} } public CSeqQueue(int maxsize){ data=new T[maxsize]; this.maxsize = maxsize; front=rear=-1; } public T this[int index]{ get{return data[(index+Front)%maxsize];} set{data[(index+Front)%maxsize]=value;} } // 求队列的长度 public int GetLength(){ return (rear+maxsize-front)%maxsize; } // 判断对列是否为空 public bool IsEmpty(){ return front == rear; } // 清空队列 public void Clear(){ front = rear = -1; } //判断循环顺序队列是否为满 public bool IsFull(){ return (rear+1)%maxsize == front; } // 入队 public void In(T item){ if(IsFull()) throw new Exception("队列已满"); data[++Rear]=item; } // 出队 public T Out(){ if(IsEmpty()) throw new Exception("队列是空的"); return data[++Front]; } // 取对头元素 public T GetFront(){ if(IsEmpty()) throw new Exception("队列是空的"); return data[Front+1]; } }
链队列(Linked Queue))
同链栈一样,链队列通常用单链表来表示,它的实现是单链表的简化。
public class LinkQueue<T> : IQueue<T> { private Node<T> front; //队列头指示器 private Node<T> rear; //队列尾指示器 private int num; //队列结点个数 public Node<T> Front{ get{return front;} private set{front = value;} } public Node<T> Rear{ get{return rear;} private set{rear = value;} } public int Num{ get{return num;} private set{num = value;} } public LinkQueue(){ num=0; front=rear=null; } //求队列的长度 public int GetLength(){ return num; } //判断对列是否为空 public bool IsEmpty(){ return num == 0; } //清空队列 public void Clear(){ num=0; front=rear=null; } //入队 public void In(T item){ var node = new Node<T>(item); if(rear==null) { front = rear = node; }else{ rear.Next = node; rear = node; } num++; } //出队 public T Out(){ if(IsEmpty()){ throw new Exception("队列是空的"); } var node= front; front = front.Next; num--; return node.Data; } //取对头元素 public T GetFront(){ if(IsEmpty()){ throw new Exception("队列是空的"); } return front.Data; } }
队的应用
编程判断一个字符串是否是回文。回文是指一个字符序列以中间字符为基准两边字符完全相同,如字符序列“ACBDEDBCA”是回文。
算法思想:判断一个字符序列是否是回文,就是把第一个字符与最后一个字符相比较,第二个字符与倒数第二个字符比较,依次类推,第i个字符与第n-i个字符比较。如果每次比较都相等,则为回文,如果某次比较不相等,就不是回文。因此,可以把字符序列分别入队列和栈,然后逐个出队列和出栈并比较出队列的字符和出栈的字符是否相等,若全部相等则该字符序列就是回文,否则就不是回文。
public bool IsPlalindrome(string str){ SeqStack<char> stack = new SeqStack<char>(str.Lenght); CSeqQueue<char> queue = new CSeqQueue<char>(str.Lenght); for(int i=0;i<str.Length;i++){ stack.Push(str[i]); queue.Push(str[i]); } while(!stack.IsEmpty()&&!queue.IsEmpty()){ if(stack.Pop()!=queue.Pop()) break; } return stackIsEmpty()&&queue.IsEmpty(); }
字符串和数组
**字符串(string)**是一种特殊的线性表,其特殊性在于串中的数据元素是一个个的字符。字符串(String)由n(n≥0)字符组成的有限序列。
字符串中任意个连续的字符组成的子序列称为该字符串的子字符串(Substring)。包含子字符串的字符串相应地称为主字符串。子字符串的第一个字符在主字符串中的位置叫子字符串的位置。
字符串的实现
public class StringDS{ private char[] data; //字符数组 public int Length{ get{return data.Length;} } // 索引 public char this[index]{ get{return data[index];} } public StringDS(int length){ data = new data[length]; } public StringDS(char[] chars){ data = new char[chars.Length]; for(int i=0;i<chars.Length;i++){ data[i]=chars[i]; } } public StringDS(string str){ data = new char[str.Length]; for(int i=0;i<str.Length;i++){ data[i]=str[i]; } } // 字符串比较 // 字符串比参数大 返回1 // 字符串比参数小 返回-1 // 字符串与参数相等 返回0 public int Compare(StringDS str){ int len=this.Length<=tr.Length?this.Length:str.Length; int i=0; while(i<len){ if(this[i] != str[i]) break; i++; } if(i<len){ if(this[i] < str[i]) return -1; else if(this[i] > str[i]) return 1; }else{ if(this.Length == str.Length) return 0; return this.Lenth > str.Length ? 1 : -1; } } //求子串 public StringDS SubString(int index, int len){ if (index < 0 || index >= this.Length || len<0 || index + len>this.Length) { throw new Exception("传入参数不正确!"); } char[] data = new char[len]; for(int i=0;i<len;i++){ data[i]=this.data[i+index]; } return new StringDS(data); } //连接字符串 public StringDS Concat(StringDS s){ char[] data = new char[s.Length+this.Length]; for(int i=0;i<this.Length;i++){ data[i]=this[i]; } for(int i=0;i<s.Length;i++){ data[i+this.Length]=s[i]; } return new StringDS(data); } //串插入 public StringDS Insert(int index, StringDS s){ int len1 = this.Length; int len2 = s.Length; if (index < 0 || index >= this.Length) { throw new Exception("传入参数不正确!"); } char[] data = new char[len1+len2]; for(int i=0;i<index+1;i++){ data[i]=this[i]; } for(int i=0;i<len2;i++){ data[i+index+1]=s[i]; } for(int i=index+1;i<len1,i++){ data[len2+i]=this[i]; } return new StringDS(data); } //串删除 public StringDS Delete(int index, int len){ int len1 = this.Length; if (index < 0 || index >= len1 || len < 0 || index + len > len1) { throw new Exception("传入参数不正确!"); } char[] data = new char[len1+len]; for(int i = 0; i < index; i++){ data[i]=this[i]; } for(int i = 0; i< len1 - len - index; i++){ data[i+index]=this[i+index+len]; } return new StringDS(data); } // 定位 public int Index(StringDS s){ int len1 = this.Length; int len2 = s.Length; if(len1 < len2) return -1; int i=0; while(i < len1 - len2){ // 生成长度相同新的字符串进行比较 StringDS ns = SubString(i,len2); if(ns.Compare(s) == 0) return i; i++; } return -1; } }
数组
数组是一种常用的数据结构,可以看作是线性表的推广。
数组的逻辑结构
数组是n(n≥1)个相同数据类型的数据元素的有限序列。一维数组可以看作是一个线性表,二维数组可以看作是“数据元素是一维数组”的一维数组,三维数组可以看作是“数据元素是二维数组”的一维数组,依次类推。
数组是一个具有固定格式和数量的数据有序集,每一个数据元素通过唯一的下标来标识和访问。通常,一个数组一经定义,每一维的大小及上下界都不能改变。所以,在数组上不能进行插入、删除数据元素等操作。
数组上的操作一般有:
- 取值操作:给定一组下标,读其对应的数据元素;
- 赋值操作:给定一组下标,存储或修改与其对应的数据元素;
- 清空操作:将数组中的所有数据元素清除;
- 复制操作:将一个数组的数据元素赋给另外一个数组;
- 排序操作:对数组中的数据元素进行排序,这要求数组中的数据元素是可排序的;
- 反转操作:反转数组中数据元素的顺序。
数组的内存映象
通常,采用顺序存储结构来存储数组中的数据元素,因为数组中的元素要求连续存放。
本质上,计算机的内存是一个一维数组,内存地址就是数组的下标。所以,对于一维数组,可根据数组元素的下标得到它的存储地址,也可根据下标来访问一维数组中的元素。
而对于多维数组,需要把多维的下标表达式转换成一维的下标表达式。当行列固定后,要用一组连续的存储单元存放数组中的元素,有一个次序约定问题,这产生了两种存储方式:一种是以行序为主序(先行后列)的顺序存放,另一种是以列序为主序(先列后行)的顺序存放。
设有二维数组 [[a11,a12...a1n],[a21,a22...a2n]...[am1,am2...amn]]
,他在内存中存放示意图:
- 行序为主序:
[a11,a12...a1n,a21,a22...a2n...am1,am2...amn] - 列序为主序:
[a11,a21...am1,a12,a22...am2...a1n,a2n...amn]
按元素的下标求地址,设数组的基址是Loc(a11),每个数据元素占w个存储单元:
-
行序为主序:
Loc(aij)=Lac(a11)+((i-1)*n+j-1)*w
因为数组元素aij的前面有i-1行,每一行有n个数据元素,在第i行中aij的前面还有j-1个元素。
-
列序为主序
Loc(aij)=Lac(a11)+((j-i)*m+i-1)*w
这是因为数组元素aij的前面有j-1列,每一列有m个数据元素,在第j列中aij的前面还有i-1个元素。
由以上的公式可知,数组元素的存储位置是其下标的线性函数,一旦确定了数组各维的长度,就可以计算任意一个元素的存储地址,并且时间相等。所以,存取数组中任意一个元素的时间也相等,因此,数组是一种随机存储结构。
树和二叉树
树
树的定义
**树(Tree)**是n(n≥0)个相同类型的数据元素的有限集合。树中的数据元素叫结点(Node)。n=0的树称为空树(Empty Tree);对于n>0的任意非空树T有:
- 有且仅有一个特殊的结点称为树的根(Root)结点,根没有前驱结点;
- 若n>1,则除根结点外,其余结点被分成了m(m>0)个互不相交的集合T1,T2,…,Tm,其中每一个集合Ti(1≤i≤m)本身又是一棵树。树T1,T2,…,Tm称为这棵树的子树(Subtree)。
树的相关术语
- 结点(Node):表示树中的数据元素,由数据项和数据元素之间的关系组成。
- 结点的度(Degree of Node):结点所拥有的子树的个数。
- 树的度(Degree of Tree):树中各结点度的最大值。
- 叶子结点(Leaf Node):度为0的结点,也叫终端结点。
- 分支结点(Branch Node):度不为0的结点,也叫非终端结点或内部结点
- 孩子(Child):结点子树的根
- 双亲(Parent):结点的上层结点叫该结点的双亲
- 祖先(Ancestor):从根到该结点所经分支上的所有结点
- 子孙(Descendant):以某结点为根的子树中的任一结点
- 兄弟(Brother):同一双亲的孩子
- 结点的层次(Level of Node):从根结点到树中某结点所经路径上的分支数称为该结点的层次。根结点的层次规定为1,其余结点的层次等于其双亲结点的层次加1。
- 堂兄弟(Sibling):同一层的双亲不同的结点
- 树的深度(Depth of Tree):树中结点的最大层次数
- 无序树(Unordered Tree):树中任意一个结点的各孩子结点之间的次序构成无关紧要的树。通常树指无序树。
- 有序树(Ordered Tree):树中任意一个结点的各孩子结点有严格排列次序的树。二叉树是有序树,因为二叉树中每个孩子结点都确切定义为是该结点的左孩子结点还是右孩子结点。
- 森林(Forest):m(m≥0)棵树的集合。
树的基本操作
- Root():求树的根结点,如果树非空,返回根结点,否则返回空;
- Parent(t):求结点t的双亲结点。如果t的双亲结点存在,返回双亲结点,否则返回空;
- Child(t,i):求结点t的第i个子结点。如果存在,返回第i个子结点,否则返回空;
- RightSibling(t):求结点t第一个右边兄弟结点。如果存在,返回第一个右边兄弟结点,否则返回空;
- Insert(s,t,i):把树s插入到树中作为结点t的第i棵子树。成功返回true,否则返回false;
- Delete(t,i):删除结点t的第i棵子树。成功返回第i棵子树的根结点,否则返回空;
- Traverse(TraverseType):按某种方式遍历树;
- Clear():清空树;
- IsEmpty():判断树是否为空树。如果是空树,返回true,否则返回false;
- GetDepth():求树的深度。如果树不为空,返回树的层次,否则返回0。
二叉树
二叉链表存储结构的类实现
public class Node<T>{ public T Data{get; private set;} public Node<T> LChild{get; private set;} public Node<T> RChild{get; private set;} public Node(T data,Node<T> lChild,Node<T> rChild){ Data = data; LChild = lChild; RChild = RChild; } public Node(T data){ Data = data; } } public class BiTree<T>{ public Node<T> Head{get; private set;} public BiTree(){} public BiTree(T t){ Head = new Node<T>(t); } public BiTree(T t,Node<T> lc,Node<T> rc){ Head = new Node<T>(t,lc,rc); } //判断是否是空二叉树 public bool IsEmpty(){ return Head == null; } //获取根结点 public Node<T> Root(){ return Head; } //获取结点的左孩子结点 public Node<T> GetLChild(Node<T> p){ return p.LChild; } //获取结点的右孩子结点 public Node<T> GetRChild(Node<T> p){ return p.RChild; } //将结点p的左子树插入值为val的新结点, //原来的左子树成为新结点的左子树 public void InsertL(T val, Node<T> p){ var tmp = new Node<T>(val); tmp.LChild = p.LChild; p.LChild = tmp; } //将结点p的右子树插入值为val的新结点, //原来的右子树成为新结点的右子树 public void InsertR(T val, Node<T> p){ var tmp = new Node<T>(val); tmp.RChild = p.RChild; p.RChild = tmp; } //若p非空,删除p的左子树 public Node<T> DeleteL(Node<T> p){ if(p == null || p.LChild == null){ return null; } var tmp = p.LChild; p.LChild = null; return tmp; } //若p非空,删除p的右子树 public Node<T> DeleteR(Node<T> p){ if(p == null || p.RChild == null){ return null; } var tmp = p.RChild; p.RChild = null; return tmp; } //判断是否是叶子结点 public bool IsLeaf(Node<T> p){ return p != null && p.RChild == null && p.LChild == null; } }
二叉树的遍历
二叉树的遍历是指按照某种顺序访问二叉树中的每个结点,使每个结点被访问一次且仅一次。
实际上,遍历是将二叉树中的结点信息由非线性排列变为某种意义上的线性排列。也就是说,遍历操作使非线性结构线性化。
先序遍历(DLR)
首先访问根结点,然后先序遍历其左子树,最后先序遍历其右子树。
public void PreOrder(Node<T> root){ if(root == null) return; // 处理根结点 Console.WriteLine(rort.Data); //先序遍历左子树 PreOrder(root.LChild); //先序遍历右子树 PreOrder(root.RChild); }
中序遍历(LDR)
中序遍历的基本思想是:首先中序遍历根结点的左子树,然后访问根结点,最后中序遍历其右子树
public void InOrder(Node<T> root){ if(root == null) return; //中序遍历左子树 InOrder(root.LChild); // 处理根结点 Console.WriteLine(rort.Data); //中序遍历右子树 InOrder(root.RChild); }
后序遍历(LRD)
后序遍历的基本思想是:首先后序遍历根结点的左子树,然后后序遍历根结点的右子树,最后访问根结点
public void InOrder(Node<T> root){ if(root == null) return; //后序遍历左子树 InOrder(root.LChild); //后序遍历右子树 InOrder(root.RChild); // 处理根结点 Console.WriteLine(rort.Data); }
层序遍历(Level Order)
层序遍历的基本思想是:由于层序遍历结点的顺序是先遇到的结点先访问,与队列操作的顺序相同。所以,在进行层序遍历时,设置一个队列,将根结点引用入队,当队列非空时,循环执行以下三步:
- 从队列中取出一个结点引用,并访问该结点;
- 若该结点的左子树非空,将该结点的左子树引用入队;
- 若该结点的右子树非空,将该结点的右子树引用入队;
public void LevelOrder(Node<T> root){ if(root == null) return; //设置一个队列保存层序遍历的结点 CSeqQueue<Node<T>> sq = new CSeqQueue<Node<T>>(50); sq.In(root); while(!sq.IsEmpty()){ // 出队 var node = sq.Out(); // 处理结点 Console.WriteLine(node.Data); if(node.LChild != null) sq.In(node.LChid); if(node.RChild != null) sq.In(node.RChild) } }
哈夫曼树
哈夫曼树的基本概念
定义哈夫曼树所要用到的几个基本概念:
- 路径(Path):从树中的一个结点到另一个结点之间的分支构成这两个结点间的路径
- 路径长度(Path Length):路径上的分支数。
- 树的路径长度(Path Length of Tree):从树的根结点到每个结点的路径长度之和。在结点数目相同的二叉树中,完全二叉树的路径长度最短。
- 结点的权(Weight of Node):在一些应用中,赋予树中结点的一个有实际意义的数。
- 结点的带权路径长度(Weight Path Length of Node):从该结点到树的根结点的路径长度与该结点的权的乘积。
- 树的带权路径长度(WPL):树中所有叶子结点的带权路径长度之和。
哈夫曼树(Huffman Tree),又叫最优二叉树,指的是对于一组具有确定权值的叶子结点的具有最小带权路径长度的二叉树。
那么,如何构造一棵哈夫曼树呢?哈夫曼最早给出了一个带有一般规律的算法,俗称哈夫曼算法。现叙述如下:
- 根据给定的n个权值{w1,w2,…,wn},构造n棵只有根结点的二叉树集合F={T1,T2,…,Tn};
- 从集合F中选取两棵根结点的权最小的二叉树作为左右子树,构造一棵新的二叉树,且置新的二叉树的根结点的权值为其左、右子树根结点权值之和。
- 在集合F中删除这两棵树,并把新得到的二叉树加入到集合F中;
- 重复上述步骤,直到集合中只有一棵二叉树为止,这棵二叉树就是哈夫曼树。
哈夫曼树的实现
// 哈夫曼树结点类 public class Node{ // 结点权值 public int Weight{get;private set;} // 左子结点在数组中位置 public int LChild{get;private set;} // 右子结点在数组中位置 public int RChild{get;private set;} // 父结点在数组中位置 public int Parent{get;private set;} public Node(){ Weight = 0; LChild = -1; RChild = -1; Parent = -1; } public Node(int weight,int lChild,int rChild,int parent){ Weight = weight; LChild = lChild; RChild = rChild; Parent = parent; } // 设置权重 public void SetWeight(int weight){ Weight = weight; } } // 哈夫曼树 public class HuffmanTree{ public Node[] Data{get;private set;} // 结点数组 public int LeafNum{get;private set;} // 叶结点数 public HuffmanTree(int lefNum){ Data = new Node[2*lefNum - 1]; for(int i = 0; i < lefNum; i++){ Data[i] = new Node(); } LefNum = lefNum; SetNodeWeight(); } public Node Create(){ for(int i = 0; i < LefNum - 1; i++){ int max1 = max2 = int.MaxValue; int tmp1,tmp2; // 获取最小权值的两个结点 for(int j = 0; j < LefNum + i; j++){ if(Data[j].Weight < max1 && Data[j].Parent == -1){ tmp2 = tmp1; max2 = max1; tmp1 = j; max1 = tmp1.Weight; }else if(Data[j].Weight < max2 && Data[j].Parent == -1){ tmp2 = j; max2 = tmp2.Weight; } } data[tmp1].Parent = LeafNum + i; data[LeafNum + i].Weight = data[tmp1].Weight + data[tmp2].Weight; data[LeafNum + i].LChild = tmp1; data[LeafNum + i].RChild = tmp2; } return data[2*LefNum-2]; } // 设置结点权值 public void SetNodeWeight(){ Random rd = new Random(); for(int i = 0; i < LeafNum; i++){ Data[i].SetWeight(rd.Next()); } } }
哈夫曼树可用于构造总长度最短的编码方案
图
图状结构简称图,是另一种非线性结构,它比树形结构更复杂。图中的顶点(把图中的数据元素称为顶点)是多对多的关系,即顶点间的关系是任意的,图中任意两个顶点之间都可能相关。
图的基本概念
图的定义
**图(Graph)是由非空的顶点(Vertex)集合和描述顶点之间的关系边(Edge)或弧(Arc)**的集合组成。其形式化定义为:
- G=(V,E)
- V={vi|vi∈某个数据元素集合}
- E={(vi,vj)|vi,vj∈V∧P(vi,vj)} 或 E={<vi,vj>|vi,vj∈V∧P(vi,vj)}
图的基本操作
// 顶点类 public class Node<T>{ // 数据域 public T Data{get;protected set;} public Node(T data){ Data = data; } } // 图的接口定义 public interface IGraph<T>{ // 获取顶点数 int GetNumOfVertex(); // 获取边或弧的数目 int GetNumOfEdge(); // 在两个顶点之间添加权值为v的边或弧 void SetEdge(Node<T> v1,Node<T> v2,int v); // 删除两个顶点之间的边或弧 void DelEdge(Node<T> v1,Node<T> v2); //判断两个顶点之间是否有边或弧 bool IsEdge(Node<T> v1, Node<T> v2); }
图的存储结构
图是一种复杂的数据结构,顶点之间是多对多的关系,即任意两个顶点之间都可能存在联系。所以,无法以顶点在存储区的位置关系来表示顶点之间的联系,即顺序存储结构不能完全存储图的信息,但可以用数组来存储图的顶点信息。要存储顶点之间的联系必须用链式存储结构或者二维数组。
图的存储结构有多种,这里只介绍两种基本的存储结构:邻接矩阵和邻接表。
邻接矩阵
邻接矩阵(Adjacency Matrix) 是用两个数组来表示图,一个数组是一维数组,存储图中顶点的信息,一个数组是二维数组,即矩阵,存储顶点之间相邻的信息,也就是边(或弧)的信息。
假设图G=(V,E)中有n个顶点,即V={v0,v1,…,vn-1},用矩阵A[i][j]表示边(或弧)的信息。矩阵A[i][j]是一个n×n的矩阵。
A[i][j]的值为:有边时,如果有权值则为权值,没有则为1,无边时,值为0。
无向图邻接矩阵类GraphAdjMatrix<T>
的实现如下所示。
public class GraphAdjMatrix<T> : IGraph<T>{ public Node<T>[] Nodes{get;private set;} // 顶点数组 public int NumEdges{get;private set;} // 边的数目 public int[,] Matrix{get;private set;} // 邻接矩阵 public GraphAdjMatrix(int n){ Nodes = new Node<T>[n]; NumEdges = 0; Matrix = new int[n,n]; } // 获取顶点数 public int GetNumOfVertex(){ return Nodes.Length; } // 获取边或弧的数目 public int GetNumOfEdge(){ return NumEdges; } // 判断v是否是图的顶点 public bool IsNode(Node<T> v) { // 遍历顶点数组 foreach (Node<T> nd in nodes) { // 如果顶点nd与v相等,则v是图的顶点,返回true if (v.Equals(nd)) { return true; } } return false; } // 获取顶点v在顶点数组中的索引 public int GetIndex(Node<T> v) { int i = -1; // 遍历顶点数组 for (i = 0; i < nodes.Length; ++i) { // 如果顶点v与nodes[i]相等,则v是图的顶点,返回索引值i。 if (nodes[i].Equals(v)) { return i; } } return i; } // 在两个顶点之间添加权值为v的边或弧 public void SetEdge(Node<T> v1,Node<T> v2,int v,bool directed = false){ Matrix[GetIndex(v1),GetIndex(v2)] = v; if(directed) Matrix[GetIndex(v2),GetIndex(v1)] = v; NumEdges++; } // 删除两个顶点之间的边或弧 public void DelEdge(Node<T> v1,Node<T> v2,bool directed = false){ Matrix[GetIndex(v1),GetIndex(v2)] = 0; if(directed) Matrix[GetIndex(v2),GetIndex(v1)] = 0; NumEdges--; } // 判断两个顶点之间是否有边或弧 public bool IsEdge(Node<T> v1, Node<T> v2){ return Matrix[GetIndex(v1),GetIndex(v2)] > 0; } }
邻接表
邻接表(Adjacency List)是图的一种顺序存储与链式存储相结合的存储结构,类似于树的孩子链表表示法。
顺序存储用于存储图的顶点,元素有两个域,一个是数据域data,存放与顶点相关的信息,一个是引用域firstAdj,存放该顶点的邻接表的第一个结点的地址。
链式存储用于存储该顶点的邻接表的结点地址。它有两个域,一个是邻接顶点域adjvex,存放邻接顶点的信息,实际上就是邻接顶点在顶点数组中的序号;一个是引用域next,存放下一个邻接顶点的结点的地址。
类实现
// 邻接顶点 public class AdjListNode{ // 邻接顶点地址 public int Adjvex{get;private set;} // 下一个邻接表结点 public AdjListNode Next{get;private set;} // 邻表顶点信息 例如权值 public object Info{get;private;} public int Num{get;private set;} public AdjListNode(){} public AdjListNode(int adjvex,object info){ Adjvex = adjvex; Next = null; Info = info; } public AdjListNode(int adjvex,adjListNode next,object info){ Adjvex = adjvex; Next = next; Info = info; } // 入栈操作 public void Push(int adjvex,object info){ AdjListNode p = new AdjListNode(item); p.Next = Next; Next = p; Num++; } // 出栈操作 public AdjListNode Pop(){ if(Next==null) return null; AdjListNode tmp = Next; Next = Next.Next; Num--; return tmp; } public void Remove(AdjListNode adjNode){ if(Next == null || adjNode == null) return; if(Next == adjNode) Next = null; AdjListNode tmp = Next; while(tmp != null){ if(tmp.Next != null && adjNode.Adjvex == tmp.Next.Adjvex){ tmp.Next = tmp.Next.Next; Num--; }else{ tmp = tmp.Next; } } } public void Remove(int adjvex){ if(Next == null || adjvex == null) return; if(Next.Adjvex == adjvex) Next = null; AdjListNode tmp = Next; while(tmp != null){ if(tmp.Next != null && adjvex == tmp.Next.Adjvex){ tmp.Next = tmp.Next.Next; Num--; }else{ tmp = tmp.Next; } } } } // 顶点结点 public class VexNode<T> : Node<T>{ // // 图的顶点 // public Node<T> Data{get;protected set;} // 邻接表的第1个结点 public AdjListNode FirstAdj{get;protected set;} public VexNode(){ Data = null; FirstAdj = null; } public VexNode(T data) : base(data) { firstAdj = null; } public VexNode(T t,AdjListNode fAdj){ Data = new Node<T>(t); firstAdj = fAdj; } } public class GraphAdjList<T> : IGraph<T>{ private VexNode<T>[] adjList; public VexNode this[index]{ get{return adjList[index];} set{adjList[index] = value;} } public GraphAdjList(T[] nodes){ this.adjList = new VexNode<T>[nodes.Length]; for(int i = 0; i < nodes.Length; i++){ adjList[i] = new AdjListNode(nodes[i]); } } // 获取顶点数 public int GetNumOfVertex(){ return adjList.Length; } // 获取边或弧的数目 public int GetNumOfEdge(){ int nums =0; for(int i = 0; i < adjList.Length; i++){ if(adjList[i].FirstAdj != null) nums += adjList[i].FirstAdj.Num; } return nums / 2; } //判断v是否是图的顶点 public bool IsNode(VexNode<T> v){ foreach(VexNode<T> nd in adjList){ if(nd.Data.Equals(v.Data)){ return true; } } return false; } //判断v是否是图的顶点 public bool IsNode(T v){ foreach(VexNode<T> nd in adjList){ if(nd.Data.Equals(v)){ return true; } } return false; } //获取顶点v在邻接表数组中的索引 public int GetIndex(VexNode<T> v){ int index=0; foreach(VexNode<T> nd in adjList){ if(nd.Data.Equals(v.Data)){ return index; } index++; } return -1; } //获取顶点v在邻接表数组中的索引 public int GetIndex(T v){ int index=0; foreach(VexNode<T> nd in adjList){ if(nd.Data.Equals(v)){ return index; } index++; } return -1; } // 在两个顶点之间添加权值为v的边或弧 public void SetEdge(Node<T> v1,Node<T> v2,int v){ if(!(v1 is VexNode<T>)) throw new Exceptions("结点v1不是图的顶点"); if(!(v2 is VexNode<T>)) throw new Exceptions("结点v2不是图的顶点"); int vn1Index = GetIndex(v1.Data); int vn2Index = GetIndex(v2.Data); if(vn1Index < 0) throw new Exceptions("结点v1不是图的顶点"); if(vn2Index < 0) throw new Exceptions("结点v2不是图的顶点"); AdjListNode aNode1 = new AdjListNode(vn1Index,v); AdjListNode aNode2 = new AdjListNode(vn2Index,v); VexNode<T> vn1 = this[vn1Index]; VexNode<T> vn2 = this[vn2Index]; if(!IsEdge(v1,v2)){ if(vn1.FirstAdj == null) vn1.FirstAdj = adNode2; else vn1.FirstAdj.Push(adNode2); } if(!IsEdge(v2,v1)){ if(vn2.FirstAdj == null) vn2.FirstAdj = adNode1; else vn2.FirstAdj.Push(adNode1); } } // 删除两个顶点之间的边或弧 public void DelEdge(Node<T> v1,Node<T> v2){ if(!(v1 is VexNode<T>)) throw new Exceptions("结点v1不是图的顶点"); if(!(v2 is VexNode<T>)) throw new Exceptions("结点v2不是图的顶点"); int vn1Index = GetIndex(v1.Data); int vn2Index = GetIndex(v2.Data); if(vn1Index < 0) throw new Exceptions("结点v1不是图的顶点"); if(vn2Index < 0) throw new Exceptions("结点v2不是图的顶点"); VexNode<T> vn1 = this[vn1Index]; VexNode<T> vn2 = this[vn2Index]; if(vn1.FirstAdj == null) throw new Exceptions("图顶点v1没有边"); if(vn2.FirstAdj == null) throw new Exceptions("图顶点v2没有边"); if(IsEdge(v1,v2)){ vn1.Remove(vn2Index); } if(IsEdge(v2,v1)){ vn2.Remove(vn1Index); } } //判断两个顶点之间是否有边或弧 public bool IsEdge(Node<T> v1, Node<T> v2){ if(!(v1 is VexNode<T>)) throw new Exceptions("结点v1不是图的顶点"); if(!(v2 is VexNode<T>)) throw new Exceptions("结点v2不是图的顶点"); int vn1Index = GetIndex(v1.Data); int vn2Index = GetIndex(v2.Data); if(vn1Index < 0) throw new Exceptions("结点v1不是图的顶点"); if(vn2Index < 0) throw new Exceptions("结点v2不是图的顶点"); VexNode<T> vn1 = this[vn1Index]; AdjListNode<T> p = vn1.FirstAdj; while(p != null){ if(p.Adjvex == vn2Index) return true; p = p.Next; } return false; } }
图的遍历
图的遍历是指从图中的某个顶点出发,按照某种顺序访问图中的每个顶点,使每个顶点被访问一次且仅一次。为了避免同一顶点被访问多次,在遍历图的过程中,必须记下每个已访问过的顶点。为此,可以设一个辅助数组visited[n],n为图中顶点的数目。数组中元素的初始值全为0,表示顶点都没有被访问过,如果顶点vi被访问,visited[i-1]为1。
图的遍历有深度优先遍历和广度优先遍历两种方式,它们对图和网都适用。
深度优先遍历
图的深度优先遍历(Depth_First Search)类似于树的先序遍历,是树的先序遍历的推广。
假设初始状态是图中所有顶点未曾被访问过,则深度优先遍历可从图中某个顶点v出发,访问此顶点,然后依次从v的未被访问的邻接顶点出发深度优先遍历图,直至图中所有和v有路径相通的顶点都被遍历过。若此时图中尚有未被访问的顶点,则另选图中一个未被访问的顶点作为起始点,重复上述过程,直到图中所有顶点都被访问到为止。
在类GraphAdjList
添加成员变量visited
private int[] visited;
在构造函数中初始化成员变量visited
public GraphAdjList(T[] nodes){ this.adjList = new VexNode<T>[nodes.Length]; this.visited = new int[nodes.Length]; for(int i = 0; i < nodes.Length; i++){ adjList[i] = new AdjListNode(nodes[i]); visited[i] = 0; } }
无向图的深度优先遍历算法的实现如下:
public void DFS(){ for(int i = 0;i < visited.Lenght; i++){ // 从没有遍历过的顶点开始遍历 if(visted[i] == 0){ DFSAL(i); } } } public void DFSAL(int i){ /* * 一些其他操作 */ // 标记顶点已被遍历 visted[p.Adjvex] = 1; AdjListNode<T> p = this[i].FirstAdj; // 使用遍历递归,防止 邻接顶点链A-B-C-D 邻接顶点链B-A-C-D的情况,导致C-D无法访问 while(p != null){ if(visted[p.Adjvex] == 0){ DFSAL(p.Adjvex); } p = p.Next; } }
广度优先遍历
**图的广度优先遍历(Breadth_First Search)**类似于树的层序遍历。
public void BFS(){ for(int i = 0;i < visited.Lenght; i++){ // 从没有遍历过的顶点开始遍历 if(visted[i] == 0){ BFSAL(i); } } } public void BFSAL(int i){ // 用于记录层级顶点 CSeqQueue<int> cq = new CSeqQueue<int>(visited.Length); cq.In(i); while(!cq.IsEmpty()){ int k = cq.Out(); if(visted[i] == 1) continum; /* * 一些其他操作 */ // 标记顶点已被遍历 visted[p.Adjvex] = 1; AdjListNode<T> p = this[k].FirstAdj; while(p != null){ if(p.Adjvex == 0) cq.In(p.Adjvex); p = p.Next; } } }
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· 开发者必知的日志记录最佳实践
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· Manus的开源复刻OpenManus初探
· AI 智能体引爆开源社区「GitHub 热点速览」
· C#/.NET/.NET Core技术前沿周刊 | 第 29 期(2025年3.1-3.9)
· 从HTTP原因短语缺失研究HTTP/2和HTTP/3的设计差异