数据结构2 - 线性表
1.顺序表#
只考虑线性表的基本操作,所以以C#接口的形式表示线性表,接口中的方法成员表示基本操作。
1.1 公共接口#
public interface IListDS<T>
{
T this[int index] { get; } //索引器
int GetLength(); //求长度
void Clear(); //清空操作
bool IsEmpty(); //判断线性表是否为空
void Append(T item); //附加操作
void Insert(T item, int i); //插入操作
T Delete(int i); //删除操作
T GetElem(int i); //取表元
int Locate(T value); //按值查找
}
为了和.NET框架中的接口IList相区分,在IList后面加了“DS”,“DS”表示数据结构。
1、求长度:GetLength()
初始条件:线性表存在;
操作结果:返回线性表中所有数据元素的个数。
2、清空操作:Clear()
初始条件:线性表存在且有数据元素;
操作结果:从线性表中清除所有数据元素,线性表为空。
3、判断线性表是否为空:IsEmpty()
初始条件:线性表存在;
操作结果:如果线性表为空返回true,否则返回false。
4、附加操作:Append(T item)
初始条件:线性表存在且未满;
操作结果:将值为item的新元素添加到表的末尾。
5、插入操作:Insert(T item, int i)
初始条件:线性表存在,插入位置正确()(1≤i≤n+1,n为插入前的表长)。
操作结果:在线性表的第i个位置上插入一个值为item的新元素,这样使得原序号为i,i+1,…,n的数据元素的序号变为i+1,i+2,…,n+1,插入后表长=原表长+1。
6、删除操作:Delete(int i)
初始条件:线性表存在且不为空,删除位置正确(1≤i≤n,n为删除前的表长)。
操作结果:在线性表中删除序号为i的数据元素,返回删除后的数据元素。
删除后使原序号为i+1,i+2,…,n的数据元素的序号变为i,i+1,…,n-1,删除后表长=原表长-1。
7、取表元:GetElem(int i)
初始条件:线性表存在,所取数据元素位置正确(1≤i≤n,n为线性表的表长);
操作结果:返回线性表中第i个数据元素。
8、按值查找:Locate(T value)
初始条件:线性表存在。
操作结果:在线性表中查找值为value的数据元素,其结果返回在线性表中首次出现的值为value的数据元素的序号,称为查找成功;否则,在线性表中未找到值为value的数据元素,返回一个特殊值表示查找失败。
1.2 实现类 SeqList#
/// <summary>
/// 顺序表
/// </summary>
/// <typeparam name="T"></typeparam>
public class SeqList<T> : IListDS<T>
{
private int count = 0; //顺序表的容量
private T[] data; //数组,用于存储顺序表中的数据元素
private int last; //指示顺序表最后一个元素的位置
//索引器
public T this[int index]
{
get { return data[index]; }
set { data[index] = value; }
}
public SeqList(int size)
{
data = new T[size];
count = 0;
}
public SeqList():this(10)
{
}
public int GetLength()
{
return count;
}
public void Clear()
{
this.count = 0;
}
public bool IsEmpty()
{
return count == 0;
}
public void Append(T item)
{
if (count == data.Length)// 当前数组存满
{
Console.WriteLine("List is full");
}
else
{
data[count] = item;
count++;
}
}
public void Insert(T item, int i)
{
if (count == data.Length)// 当前数组存满
{
Console.WriteLine("List is full");
}
else
{
//这里因为从插入位置往后覆盖,第1个被覆盖索引数据就不见了。所以需要从最后面往前覆盖
for (int j = count-1; j >= i; j--)
{
data[j + 1] = data[j];
}
data[i] = item;
count++;
}
}
public T Delete(int i)
{
T temp = data[i];
for (int j = i+1; j < count; j++)
{
data[j - 1] = data[j];
}
count--;
return temp;
}
public T GetElem(int i)
{
//索引存在
if (i >= 0 && i <= count-1)
{
return data[i];
}
else
{
Console.WriteLine("索引不存在");
return default(T);
}
}
public int Locate(T value)
{
for (int i = 0; i < data.Length; i++)
{
if (value.Equals(data[i]))
{
return i;
}
}
return -1;
}
}
1.3 测试:#
class Program
{
static void Main(string[] args)
{
SeqList<string> seqList = new SeqList<string>();
seqList.Append("123");
seqList.Append("456");
seqList.Append("789");
Console.Write("第一次加载:");
for (int i = 0; i < seqList.GetLength(); i++)
{
Console.Write($"{seqList[i]},");
}
Console.WriteLine();
Console.WriteLine($"第0位置元素:{seqList.GetElem(0)}");
Console.WriteLine($"第0索引:{seqList[0]}");
Console.Write("Insert后的数据:");
seqList.Insert("999", 1);
for (int i = 0; i < seqList.GetLength(); i++)
{
Console.Write($"{seqList[i]},");
}
Console.WriteLine();
Console.Write("Delete(0)后的数据:");
seqList.Delete(0);
for (int i = 0; i < seqList.GetLength(); i++)
{
Console.Write($"{seqList[i]},");
}
Console.WriteLine();
Console.Write("清空后:");
seqList.Clear();
Console.WriteLine(seqList.GetLength());
Console.Read();
}
}
2.链表#
2.1 单链表#
在对顺序表进行插入和删除时,需要通过移动数据元素来实现,影响了运行效率。本节介绍线性表的另外一种存储结构——链式存储(Linked Storage),这样的线性表叫链表(Linked List)。链表不要求逻辑上相邻的数据元素在物理存储位置上也相邻,因此,在对链表进行插入和删除时不需要移动数据元素,但同时也失去了顺序表可随机存储的优点。
2.1.1 单链表的存储#
链表是用一组任意的存储单元来存储线性表中的数据元素(这组存储单元可以是连续的,也可以是不连续的)。那么,怎么表示两个数据元素逻辑上的相邻关系呢?即如何表示数据元素之间的线性关系呢?为此,在存储数据元素时,除了存储数据元素本身的信息外,还要存储与它相邻的数据元素的存储地址信息。这两部分信息组成该数据元素的存储映像(Image),称为结点(Node)。把存储据元素本身信息的域叫结点的数据域(Data Domain),把存储与它相邻的数据元素的存储地址信息的域叫结点的引用域(Reference Domain)。因此,线性表通过每个结点的引用域形成了一根“链条”,这就是“链表”名称的由来。
如果结点的引用域只存储该结点直接后继结点的存储地址,则该链表叫单链表(Singly Linked List)。把该引用域叫 next。单链表结点的结构如图所示,图中 data 表示结点的数据域。
2.1.2 链式存储结构#
2.1.3 节点类 Node#
新建节点类,注意data与next
// <summary>
/// 单链表节点
/// </summary>
/// <typeparam name="T"></typeparam>
public class Node<T>
{
public T data { get; set; } //(数据域),存储数据本身
public Node<T> next { get; set; } //(引用域)指针,用来指向下一个元素
public Node(T data)
{
this.data = data;
this.next = null;
}
public Node(Node<T> next)
{
this.next = next;
}
public Node(T data, Node<T> next)
{
this.data = data;
this.next = next;
}
}
2.1.4 实现类 SingleLinkList#
/// <summary>
/// 单链表
/// </summary>
/// <typeparam name="T"></typeparam>
class SingleLinkList<T> : IListDS<T>
{
private Node<T> header;
public int GetLength()
{
Node<T> p = header;
int len = 0;
while (p != null)
{
++len;
p = p.next;
}
return len;
}
public void Clear()
{
header = null;
}
public bool IsEmpty()
{
return header == null;
}
public void Append(T item)
{
Node<T> newNode = new Node<T>(item);
//如果头节点为空,新节点为头节点
if (header == null)
{
header = newNode;
}
else
{
Node<T> headNode = header;
//下个节点为空时,说明是末尾节点。
while (headNode.next != null)
{
headNode = headNode.next;
}
//把需要添加的node,添加给末尾指针next
headNode.next = newNode;
}
}
public void Insert(T item, int i)
{
//从第1个节点开始
if (i == 0)
{
//头节点不允许插入
Console.WriteLine("List is empty or Position is error");
}
else if (i == 1)
{
//如果是第1个节点
Node<T> newNode = new Node<T>(item);
newNode.next = header.next;
header.next = newNode;
}
else
{
Node<T> tempNode = header;
int j = 1;
//让temp从头开始向后移动到下一个位置,移动到插入前一个位置为止。
while (tempNode.next != null && j < i)
{
tempNode = tempNode.next;
j++;
}
if (j == i)
{
Node<T> newNode = new Node<T>(item); //行插入节点
Node<T> preNode = tempNode; //前节点
Node<T> currentNode = tempNode.next; //当前节点
newNode.next = currentNode; //插入节点的指针
preNode.next = newNode; //前节点指针
}
}
}
public T Delete(int i)
{
//从第1个节点开始
if (i == 0)
{
T current = header.data;
header = header.next;
return current;
}
else
{
Node<T> tempNode = header;
int j = 1;
//让temp从头开始向后移动到下一个位置,移动到插入前一个位置为止。
while (tempNode.next != null && j < i)
{
tempNode = tempNode.next;
j++;
}
if (j == i)
{
Node<T> preNode = tempNode; //前节点
Node<T> currentNode = preNode.next; //前节点
Node<T> nexNode = tempNode.next.next; //下一个节点
preNode.next = nexNode; //前节点指针
return currentNode.data;
}
return default(T);
}
}
public T GetElem(int i)
{
return this[i];
}
public int Locate(T value)
{
if (IsEmpty())
{
return -1;
}
else
{
Node<T> p = header;
int i = 1;
while (!p.data.Equals(value) && p.next != null)
{
p = p.next;
++i;
}
return i;
}
}
public T this[int index]
{
get
{
Node<T> p = header;
int len = 0;
while (p != null && len < index)
{
++len;
p = p.next;
}
return p.data;
}
}
public override string ToString()
{
StringBuilder builder = new StringBuilder();
builder.Append("单链表:");
Node<T> p = header;
int len = 0;
while (p != null)
{
builder.Append($"{p.data.ToString()},");
++len;
p = p.next;
}
return builder.ToString();
}
}
2.1.5 测试#
class Program
{
static void Main(string[] args)
{
SingleLinkList<int> singleLink = new SingleLinkList<int>();
singleLink.Append(100);
singleLink.Append(200);
singleLink.Append(300);
singleLink.Append(400);
singleLink.Append(500);
singleLink.Append(600);
Console.WriteLine(singleLink.ToString());
singleLink.Insert(900, 3);
Console.WriteLine("插入:900 index:3");
Console.WriteLine(singleLink.ToString());
Console.WriteLine("删除index:4,删除的数据为:" + singleLink.Delete(4));
Console.WriteLine(singleLink.ToString());
Console.WriteLine("现在长度:" + singleLink.GetLength());
Console.WriteLine("索引5的数据:" + singleLink[5]);
}
}
2.2 双向链表#
前面介绍的单链表允许从一个结点直接访问它的后继结点,所以, 找直接后继结点的时间复杂度是 O(1)。但是,要找某个结点的直接前驱结点,只能从表的头引用开始遍历各结点。如果某个结点的 Next 等于该结点,那么,这个结点就是该结点的直接前驱结点。也就是说,找直接前驱结点的时间复杂度是 O(n), n是单链表的长度。当然,我们也可以在结点的引用域中保存直接前驱结点的地址而不是直接后继结点的地址。这样,找直接前驱结点的时间复杂度只有 O(1),但找直接后继结点的时间复杂度是 O(n)。如果希望找直接前驱结点和直接后继结点的时间复杂度都是 O(1),那么,需要在结点中设两个引用域,一个保存直接前驱结点的地址,叫 prev,一个直接后继结点的地址,叫 next,这样的链表就是双向链表(Doubly Linked List)。双向链表的结点结构示意图如图所示。
2.2.1 双向链表节点实现#
public class DbNode<T>
{
private T data; //数据域
private DbNode<T> prev; //前驱引用域
private DbNode<T> next; //后继引用域
//构造器
public DbNode(T val, DbNode<T> p)
{
data = val;
next = p;
}
//构造器
public DbNode(DbNode<T> p)
{
next = p;
}
//构造器
public DbNode(T val)
{
data = val;
next = null;
}
![
2.3 循环链表#
有些应用不需要链表中有明显的头尾结点。在这种情况下,可能需要方便地从最后一个结点访问到第一个结点。此时,最后一个结点的引用域不是空引用,而是保存的第一个结点的地址(如果该链表带结点,则保存的是头结点的地址),也就是头引用的值。带头结点的循环链表(Circular Linked List)如图所示。
作者:【唐】三三
出处:https://www.cnblogs.com/tangge/p/9917453.html
版权:本作品采用「署名-非商业性使用-相同方式共享 4.0 国际」许可协议进行许可。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· SQL Server 2025 AI相关能力初探
· Linux系列:如何用 C#调用 C方法造成内存泄露
· AI与.NET技术实操系列(二):开始使用ML.NET
· 记一次.NET内存居高不下排查解决与启示
· 探究高空视频全景AR技术的实现原理
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· AI技术革命,工作效率10个最佳AI工具
2012-11-06 VS2010设置程序启动用管理员模式