转载,数据结构与算法----数组链表

news/2025/11/26 16:58:46/文章来源:https://www.cnblogs.com/yongwei/p/19273685

https://www.cnblogs.com/lmy5215006/p/18736066

简介

数据结构的本质,只有两种结构,数组与链表。其它的都是它的衍生与组合
算法的本质就是穷举

数组

数组可以分为两大类,静态数组动态数组
静态数组的本质是一段连续的内存,因为是连续的,所以我们可以采用偏移量的方式来对元素实现快速访问。
而动态数组则是对静态数组的封装,使得更加方便操作元素。有了动态数组,后续的栈,哈希,队列都能更加优雅的实现。

静态数组

  1. 数组的超能力
    随机访问。只要任意一个索引,都能推测出元素的内存地址,而计算机的内存寻址能力为Log(1),所以数组的随机访问时间复杂度也同样为Log(1)

  2. 数组的局限性
    由于数组的大小是固定的,所以当数组满了,或者需要在中间插入/删除时。都需要移动元素,这时候时间复杂度就上升为Log(N)

动态数组

动态数组无法解决静态数组Log(N)的问题,它只是帮你隐藏了动态扩容与元素搬移的过程,以及更加易用的API。

数组随机访问的超能力源于数组连续的内存空间,而连续的内存空间就不可避免地面对元素搬移和扩缩容的问题

一个简单的动态数组

public class MyList<T>(){//真正存储数据的底层private T[] arr = new T[5];//记录元素的数量public int Count { get; private set; }/// <summary>/// 增/// </summary>/// <param name="item"></param>public void Add(T item){if (Count == arr.Length){//扩容Resize(Count * 2);}arr[Count] = item;Count++;}/// <summary>/// 删/// </summary>/// <param name="idx"></param>public void RemoveAt(int idx){if (Count == arr.Length / 4){//缩容Resize(arr.Length / 2);}Count--;for (int i = idx; i < Count; i++){arr[i] = arr[i + 1];}arr[Count] = default(T);}public void Remove(T item){var idx = FindIndex(item);RemoveAt(idx);}/// <summary>/// 改/// </summary>/// <param name="idx"></param>/// <param name="newItem"></param>public void Put(int idx,T newItem){arr[idx] = newItem;}/// <summary>/// 查/// </summary>/// <param name="item"></param>/// <returns></returns>public int FindIndex(T item){for(int i = 0; i < arr.Length; i++){if (item.Equals(arr[i]))return i;}return -1;}/// <summary>/// 扩容/缩容操作/// </summary>/// <param name="initCapacity"></param>private void Resize(int initCapacity){var newArray=new T[initCapacity];for(var i = 0; i < Count; i++){newArray[i] = arr[i];}arr = newArray;}}

数组的变种:环形数组

有人可能会问了?数组不是一段连续的内存吗?怎么可能是环形的?
从物理角度出发,这确实不可能。但从逻辑角度出发,这是有可能的。
其核心内容就是利用求模运算

        public static void Run(){var arr = new int[] { 1, 2, 3, 4, 5, 6 };var i = 0;while (arr.Length > 0){Console.WriteLine(arr[i]);//关键代码在此,当i遍历到末尾时,i+1与arr.Length去余数变成0//从逻辑上完成了闭环i = (i + 1) % arr.Length;if ((i % arr.Length) == 0){Console.WriteLine("完成了一次循环,i归零");Thread.Sleep(1000);}}}

环形数组的关键在于,它维护了两个指针 start 和 end,start 指向第一个有效元素的索引,end 指向最后一个有效元素的下一个位置索引
环形数组解决了什么问题?数组在头部增删从O(N),优化为O(1)

一个简单的环形数组

点击查看代码
    public class CircularArray<T>: IEnumerable<T>{public static void Run(){var arr = new CircularArray<string>();arr.AddLast("4");arr.AddLast("5");arr.AddFirst("3");arr.AddFirst("2");arr.AddFirst("1");foreach (var item in arr){Console.WriteLine(item);}}private T[] _array;private int _head;private int _tail;public int Count { get; private set; }public int Capacity { get; private set; }public CircularArray(){var capacity = 10;_array = new T[capacity];_head = 0;_tail = 0;Count = 0;Capacity = capacity;}/// <summary>/// 扩容/缩容/// </summary>/// <param name="capacity"></param>private void Resize(int capacity){var newArr=new T[capacity];for(int i=0; i < Count; i++){newArr[i] = _array[(_head + i) % Capacity];}_array = newArr;//重置指针_head = 0;_tail = Count;Capacity = capacity;}/// <summary>/// 在头部添加元素/// O(1)/// </summary>/// <param name="item"></param>public void AddFirst(T item){if (Count == Capacity){Resize(Capacity * 2);}_head = (_head - 1 + Capacity) % Capacity;_array[_head] = item;Count++;}/// <summary>/// 在尾部添加元素/// </summary>/// <param name="item"></param>public void AddLast(T item){if (Count == Capacity){Resize(Capacity * 2);}_array[_tail] = item;_tail = (_tail + 1) % Capacity;Count++;}/// <summary>/// 在尾部删除/// </summary>public void RemoveLast(){_tail = (_tail - 1 + Capacity) % Capacity;_array[_tail] = default;Count--;}/// <summary>/// 在头部删除/// </summary>public void RemoveFirst(){_array[_head] = default;_head = (_head + 1) % Capacity;Count--;}/// <summary>/// 获取头部元素/// </summary>/// <returns></returns>public T GetFirst(){return _array[_head];}/// <summary>/// 获取尾部元素/// </summary>/// <returns></returns>public T GetLast(){return _array[(_tail - 1 + Capacity) % Capacity];}public T Get(int idx){return _array[idx];}public IEnumerator<T> GetEnumerator(){for (int i = 0; i < Count; i++){ var index = (_head + i) % Capacity;yield return _array[index];}}IEnumerator IEnumerable.GetEnumerator(){return GetEnumerator();}}

链表

链表分为单链表双链表,单链表只有一个指针,指向next元素。双链表有两个指针,分别指向previous与next。
除此之外并无其它区别。主要功能区别在于能否向前遍历。

为什么需要链表

前面说到,数组的本质是一段连续的内存,当元素移动/扩容时,需要one by one 移动,花销很大。
那有没有一种能突破内存限制的数据结构呢?链表就应运而生。链表不需要连续内存,它们可以分配在天南海北,它们之间的联系靠next/prev链接,将零散的元素串成一个链式结构。

这么做有两个好处

  1. 提高内存利用率,分配在哪都可以。所以可以降低内存碎片
  2. 方便扩容与移动,只需要重新指向next/previous 即可实现增,删,改等操作,无需移动元素与扩容。

但万物皆有代价,因为链表的不连续性,所以无法利用快速随机访问来定位元素,只能一个一个的遍历来确定元素。因此链表的查询复杂度为Log(N)

一个简单的链表

public class MyLinkedList<T>
{public static void Run(){var linked = new MyLinkedList<string>();linked.AddLast("a");linked.AddLast("b");linked.AddLast("c");linked.AddLast("d");linked.Add(1, "bc");linked.Put(1, "aaaa");Console.WriteLine(linked.ToString()) ;}/// <summary>/// 虚拟头尾节点,有两个好处/// 1.无论链表是否为空, 两个虚拟节点都存在,避免很多边界值处理的情况。/// 2.如果要在尾部插入数据,如果不知道尾节点,那么需要复杂度退化成O(N),因为要从头开始遍历到尾部。/// </summary>private Node _head, _tail; public int Count { get; private set; }public MyLinkedList(){_tail = new Node();_head = new Node();_head.Next = _tail;_tail.Prev = _head;}public void AddLast(T item){var prev = _tail.Prev;var next = _tail;var node = new Node(item);node.Next = next;node.Prev = prev;prev.Next = node;next.Prev = node;Count++;}public void AddFirst(T item){var prev = _head;var next = _head.Next;var node=new Node(item);node.Prev= prev;node.Next= next;prev.Next= node;next.Prev = node;Count++;}public void Add(int idx,T item){var t = Get(idx);var next = t.Next;var prev = t;var node = new Node(item);node.Next = next;node.Prev = prev;prev.Next = node;next.Prev = node;}public void Remove(int idx){var t = Get(idx);var prev = t.Prev;var next = t.Next;prev.Next = next;next.Prev = next;t = null;Count--;}public void Put(int idx,T item){var t = Get(idx);t.Value= item;}private Node? Get(int idx){var node = _head.Next;//这里有个优化空间,可以通过idx在Count的哪个区间。从而决定从head还是从tail开始遍历for (int i = 0; i < idx; i++){node = node.Next;}return node;}public override string ToString(){var sb = new StringBuilder();var node = _head.Next;while (node != null && node.Value != null){sb.Append($"{node.Value}<->");node = node.Next;}sb.Append("null");return sb.ToString();}private class Node{public T? Value { get; set; }public Node Next { get; set; }public Node Prev { get; set; }public Node(){Value=default(T);}public Node(T value){Value = value;}}
}

链表的变种:跳表

在上面简单的例子中,查询的复杂度为O(N),插入的复杂度为O(1).
主要消耗在查询操作,只能从头结点开始,逐个遍历到目标节点。
所以我们优化的重点就在于优化查询。

上面的例子中,我们使用了虚拟头尾节点来空间换时间,提高插入效率。同样的,我们也可以采用这个思路来提高查询效率

跳表核心原理

index  0  1  2  3  4  5  6  7  8  9
node   a->b->c->d->e->f->g->h->i->j

此时此刻,你想拿到h的节点,你需要从0开始遍历直到7
这时候你就想,如果我能提前知道6的位置就好了,这样我就只需要Next就能快速得到h

调表就是如此

indexLevel   0-----------------------8-----10
indexLevel   0-----------4-----------8-----10
indexLevel   0-----2-----4-----6-----8-----10
indexLevel   0--1--2--3--4--5--6--7--8--9--10
nodeLevel    a->b->c->d->e->f->g->h->i->j->k

调表在原链表的基础上,增加了多层索引,每向上一层,索引减少一半,所以索引的高度是O(log N)

  1. 首先从最高层索引开始往下搜,索引7在[0,8]区间
  2. 从节点0开始,发现7在【4,8】,拿到节点4的地址
  3. 从节点4开始,发现7在【6,8】,拿到节点6的地址
  4. 从节点6开始,发现7在【6,7】,最终找到节点7

在搜索的过程中,会经过O(log N)层索引,所以时间复杂度为O(log N)

调表实现比较复杂,当新增与删除时,还需考虑索引的动态调整,需要保证尽可能的二分,否则时间复杂度又会退化为O(N)
有点类似自平衡的二叉搜索数,不过相对来说比较简单。

一个简单的跳表

点击查看代码
public class ConcurrentSkipList<T> :ICollection<T> where T : IComparable<T>
{public class SkipListNode<T>{public T Value { get; }/// <summary>/// 每层的下一个节点指针数组/// Next[i] 表示第i层的下一个节点/// </summary>public SkipListNode<T>[] Next { get; }/// <summary>/// 每层的跨度数组/// Span[i] 表示从当前节点到Next[i]节点之间跨越的节点数(不包括Next[i])/// 例如:如果当前节点在第0层的下一个节点是第3个节点,则Span[0] = 2/// </summary>public int[] Span { get; }public SkipListNode(T value, int level){Value = value;Next = new SkipListNode<T>[level];Span = new int[level];}}/// <summary>/// 头节点,不存储实际数据,作为每层链表的起点/// </summary>private readonly SkipListNode<T> _head;private readonly Random _random;private int _maxLevel;private int _count;private const int MaxLevel = 32;private const double Probability = 0.5;/// <summary>/// 读写锁,保证线程安全/// </summary>private readonly ReaderWriterLockSlim _lock = new();public ConcurrentSkipList(){_maxLevel = 1;_head = new SkipListNode<T>(default, MaxLevel);_random = new Random();_count = 0;// 初始化头节点的每层跨度for (int i = 0; i < MaxLevel; i++){_head.Span[i] = 0;}}public int Count => _count;private int RandomLevel(){int level = 1;while (_random.NextDouble() < Probability && level < MaxLevel)level++;return level;}public void Add(T value){if (value == null)throw new ArgumentNullException(nameof(value));_lock.EnterWriteLock();try{// update数组记录每层需要更新的节点var update = new SkipListNode<T>[MaxLevel];// rank数组记录每层经过的节点数var rank = new int[MaxLevel];var current = _head;// 从最高层开始,找到每层需要更新的节点for (int i = _maxLevel - 1; i >= 0; i--){// 计算当前层经过的节点数rank[i] = i == _maxLevel - 1 ? 0 : rank[i + 1];// 在当前层找到第一个大于等于value的节点的前一个节点while (current.Next[i] != null && current.Next[i].Value.CompareTo(value) < 0){rank[i] += current.Span[i];current = current.Next[i];}update[i] = current;}// 随机决定新节点的层数int level = RandomLevel();// 如果新节点的层数大于当前最大层数,需要更新头节点if (level > _maxLevel){for (int i = _maxLevel; i < level; i++){update[i] = _head;update[i].Span[i] = _count;}_maxLevel = level;}// 创建新节点var newNode = new SkipListNode<T>(value, level);// 更新每层的指针和跨度for (int i = 0; i < level; i++){newNode.Next[i] = update[i].Next[i];update[i].Next[i] = newNode;// 更新跨度:// 1. 新节点的跨度 = 原跨度 - (rank[0] - rank[i])// 2. 更新节点的跨度 = (rank[0] - rank[i]) + 1newNode.Span[i] = update[i].Span[i] - (rank[0] - rank[i]);update[i].Span[i] = (rank[0] - rank[i]) + 1;}// 更新更高层的跨度for (int i = level; i < _maxLevel; i++){update[i].Span[i]++;}_count++;}finally{_lock.ExitWriteLock();}}public bool Remove(T value){if (value == null)throw new ArgumentNullException(nameof(value));_lock.EnterWriteLock();try{// update数组记录每层需要更新的节点var update = new SkipListNode<T>[MaxLevel];var current = _head;// 从最高层开始,找到每层需要更新的节点for (int i = _maxLevel - 1; i >= 0; i--){while (current.Next[i] != null && current.Next[i].Value.CompareTo(value) < 0){current = current.Next[i];}update[i] = current;}// 检查是否找到要删除的节点current = current.Next[0];if (current == null || current.Value.CompareTo(value) != 0)return false;// 更新每层的指针和跨度for (int i = 0; i < _maxLevel; i++){if (update[i].Next[i] == current){// 如果当前层存在要删除的节点,更新指针和跨度update[i].Span[i] += current.Span[i] - 1;update[i].Next[i] = current.Next[i];}}// 如果最高层变为空,降低最大层数while (_maxLevel > 1 && _head.Next[_maxLevel - 1] == null)_maxLevel--;_count--;return true;}finally{_lock.ExitWriteLock();}}/// <summary>/// 获取元素在跳表中的排名(从1开始)/// </summary>public int GetRank(T value){if (value == null)throw new ArgumentNullException(nameof(value));_lock.EnterReadLock();try{var current = _head;int rank = 0;// 从最高层开始,累加经过的节点数for (int i = _maxLevel - 1; i >= 0; i--){while (current.Next[i] != null && current.Next[i].Value.CompareTo(value) < 0){rank += current.Span[i];current = current.Next[i];}}// 检查是否找到目标节点current = current.Next[0];if (current != null && current.Value.CompareTo(value) == 0){rank++;return rank;}return -1;}finally{_lock.ExitReadLock();}}/// <summary>/// 获取指定范围的元素/// </summary>/// <param name="startIndex"></param>/// <param name="count"></param>public List<T> GetRange(int startIndex, int count){if (startIndex < 0 || count <= 0)return new List<T>();_lock.EnterReadLock();try{var result = new List<T>();var current = _head;int traversed = 0;// 从最高层开始,快速定位到起始位置for (int i = _maxLevel - 1; i >= 0; i--){while (current.Next[i] != null && traversed + current.Span[i] <= startIndex){traversed += current.Span[i];current = current.Next[i];}}// 从起始位置开始,顺序获取指定数量的元素current = current.Next[0];int collected = 0;while (current != null && collected < count){result.Add(current.Value);current = current.Next[0];collected++;}return result;}finally{_lock.ExitReadLock();}}public void Clear(){_lock.EnterWriteLock();try{for (int i = 0; i < _maxLevel; i++){_head.Next[i] = null;_head.Span[i] = 0;}_maxLevel = 1;_count = 0;}finally{_lock.ExitWriteLock();}}public bool Contains(T item){return GetRank(item) != -1;}public void CopyTo(T[] array, int arrayIndex){if (array == null)throw new ArgumentNullException(nameof(array));if (arrayIndex < 0)throw new ArgumentOutOfRangeException(nameof(arrayIndex));if (array.Length - arrayIndex < _count)throw new ArgumentException("index out of bounds");_lock.EnterReadLock();try{var current = _head.Next[0];while (current != null){array[arrayIndex++] = current.Value;current = current.Next[0];}}finally{_lock.ExitReadLock();}}public bool IsReadOnly => false;public IEnumerator<T> GetEnumerator(){_lock.EnterReadLock();try{var current = _head.Next[0];while (current != null){yield return current.Value;current = current.Next[0];}}finally{_lock.ExitReadLock();}}IEnumerator IEnumerable.GetEnumerator(){return GetEnumerator();}
}
 
合集: 数据结构与算法

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

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

相关文章

2025 年 DeepSeek 知识库私有化部署 (11 月更新):企业数据安全智能方案,含 AI 知识库部署方案商、Deepseek 部署服务商、BI 私有化部署厂商”

在人工智能迅猛发展的今天,企业知识管理正面临前所未有的变革与挑战。在2025年的商业环境中,企业知识库已成为组织智慧的核心载体,但随之而来的数据安全与隐私保护问题也日益凸显。越来越多的企业开始寻求既智能又安…

上海高温炉品牌推荐:聚焦工业加热设备优质选择

在工业生产领域,高温炉作为实现材料热处理、烧结、煅烧等工艺的关键设备,其性能稳定性与技术可靠性直接影响生产效率与产品质量。上海作为国内工业设备制造的核心区域,汇聚了众多专注于高温炉研发与生产的企业。本文…

2025年挖泥疏浚船订做厂家权威推荐榜单:清淤疏浚船‌/小型清淤船‌/挖泥船锚艇源头厂家精选

在航道疏浚与水利工程建设需求持续增长的背景下,挖泥疏浚船作为关键施工装备,其性能优劣直接关系到工程效率与运营成本。 挖泥疏浚船通过高效的挖掘和输送系统,在各类水域环境中完成清淤作业。据行业数据显示,2025…

2025堆垛机立体库厂家哪家好?最新堆垛机立体库制造商排名推荐与趋势综合分析

随着智能制造、工业4.0战略的深入推进,自动化立体库作为现代智慧物流体系的核心组成部分,正迎来前所未有的发展机遇。在制造业转型升级、土地资源日益紧张、人力成本持续上升的背景下,高效、智能、高密度的堆垛机立…

新手购买云服务器怎么知道配置带宽是否够用

新手必看:云服务器配置&带宽是否够用?3步快速判断! 新手判断配置和带宽是否够用,核心原则是「先匹配场景,再实测验证,最后动态调整」——不用懂复杂公式,跟着“场景对应+简单监控+阈值判断”的思路,就能精…

推荐几家高温炉定制厂家,技术实力与服务解析

在工业生产中,高温炉作为实现材料热处理、烧结、烘干等工艺的关键设备,其性能直接影响产品质量。不同行业对高温炉的温度范围、控温精度、腔体尺寸等需求存在差异,定制化服务成为满足个性化生产需求的重要选择。以下…

2025年线性霍尔元件批发厂家权威推荐榜单:双极霍尔元件/微功耗霍尔元件/单极霍尔元件源头厂家精选

在智能传感技术快速发展的背景下,线性霍尔元件作为磁感测领域的核心组件,其性能直接关系到电机控制、电流检测和位置测量等应用的精度与可靠性。现代线性霍尔元件已从基础磁场感应升级为集温度补偿、低电压运行、高灵…

国标GB28181算法算力平台EasyGBS:构建云端一体化视频监控服务的核心引擎

当前,视频监控系统正经历着从“看得见”到“看得懂”,从“分散孤立”到“融合智能”的深刻变革。传统烟囱式、孤岛式的监控架构,面临着管理分散、数据割裂、智能应用部署困难等核心痛点。在这一背景下,“云端一体化…

国产仓储软件(WMS):助力企业实现精益智能仓储管理

随着物流行业的快速发展和制造业的智能化升级,仓储管理作为供应链中的关键环节,其效率与精准度直接影响企业的运营成本与市场竞争力。国产仓储软件(WMS)凭借本土化研发优势、对国家信创战略的全面支持以及贴合国内…

k8s event 单独存储一个etcd

要将 Kubernetes 的 Event 资源单独存储到一个独立的 etcd 集群,只需在 kube-apiserver 的启动参数中增加 --etcd-servers-overrides 配置即可,无需迁移数据或重启其他组件。 配置步骤部署一套独立的 etcd 集群,用于…

国内值得关注的高温炉工厂有哪些?

高温炉作为工业生产中不可或缺的热加工设备,广泛应用于电子、汽车、材料、化工等领域,其性能直接影响产品质量与生产效率。选择专业的高温炉工厂,需综合考量技术研发实力、产品系列完整性、定制化服务能力及售后保障…

防爆烘箱厂家哪家强?国内优质厂商及设备特点解析

防爆烘箱作为化工、医药、新能源等领域的关键设备,其安全性能与运行稳定性直接关系到生产流程的顺畅与操作人员的安全。随着行业对防爆设备需求的不断提升,选择技术成熟、品质可靠的生产厂家成为企业关注的重点。本文…

汽车小型打气泵方案

打气泵是一种常见的气体压缩设备,它可以将空气或其他气体压缩成高压气体,用于充气、输送、压缩等工业和生活领域。打气泵方案是通过内部马达的运转来工作。抽气时,连通器的阀门被大气的气压冲开,气体进入气筒,而向…

3台centos上部署k8s集群

3台centos上部署k8s集群版本兼容问题 基于CentOS 7.9的默认内核版本3.10,在不升级内核的前提下,推荐安装Kubernetes 1.23.x版本CentOS 7.9默认内核版本为3.10.0-1160.el7.x86_64,这个版本对Kubernetes的兼容性存在以…

仓储软件(WMS)值得推荐的行业应用与选择参考

在现代物流与供应链管理体系中,仓储软件(WMS)作为连接采购、生产与销售的核心环节,其智能化水平直接影响企业库存周转效率、订单履约能力及综合运营成本。随着智能制造与电商行业的快速发展,具备精益管理、数据驱…

仓储软件(WMS)哪家专业?行业热门选择解析

在现代仓储管理中,仓储管理系统(WMS)作为连接供应链各环节的核心工具,其专业性直接影响企业库存周转效率、订单履约能力及整体运营成本。随着物流自动化与智能化的发展,企业对WMS的需求已从基础的库存记录转向与自…

JavaScript 事件循环,Promise 与 async/await

JavaScript 事件循环,Promise 与 async/await一、浏览器 1、浏览器的多进程 现代浏览器采用多进程+多线程架构,以提升性能、稳定性与安全性 现代浏览器的常见进程包括:1个浏览器主进程,1个GPU进程,1个网络进程,多…

《postman、apipost、smartApi 等使用与特点 3 天路线图(可打印 PDF+互动脑图)》

# postman、Apipost、foxapi、smartapi 等 API 调试工具使用以及特点3 天速成路径(每天 3 个知识点,二级列表)- 第 1 天 Postman 核心三板斧- 界面速览:Workspace → Collection → Request 三级结构与环境变量初识…

2025年11月苗木批发基地推荐,紫薇/油松/丝棉木/樱花/红叶石楠/红叶李苗木基地

行业背景与评选标准 随着城镇化进程加速和生态文明建设深入推进,苗木产业作为园林绿化的重要基础,呈现出蓬勃发展的态势。据国家林业和草原局最新数据显示,2024年全国苗木种植面积已突破2000万亩,年产值超过3000亿…

特征选择F检验

一、简介谢谢!