简介

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

算法的本质就是穷举

数组

数组可以分为两大类,静态数组动态数组

静态数组的本质是一段连续的内存,因为是连续的,所以我们可以采用偏移量的方式来对元素实现快速访问。

而动态数组则是对静态数组的封装,使得更加方便操作元素。有了动态数组,后续的栈,哈希,队列都能更加优雅的实现。

静态数组

  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)

链表

链表分为单链表双链表,单链表只有一个指针,指向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)

有点类似自平衡的二叉搜索数,不过相对来说比较简单。

重生之数据结构与算法----数组&链表的更多相关文章

  1. JavaScript数据结构与算法-数组练习

    一. 创建一个记录学生成绩的对象,提供一个添加成绩的方法,以及一个显示学生平均成绩的方法. // 创建一个记录学生成绩的对象 const Students = function Students () ...

  2. Java数据结构和算法(四)--链表

    日常开发中,数组和集合使用的很多,而数组的无序插入和删除效率都是偏低的,这点在学习ArrayList源码的时候就知道了,因为需要把要 插入索引后面的所以元素全部后移一位. 而本文会详细讲解链表,可以解 ...

  3. C语言 - 基础数据结构和算法 - 单向链表

    听黑马程序员教程<基础数据结构和算法 (C版本)>,照着老师所讲抄的, 视频地址https://www.bilibili.com/video/BV1vE411f7Jh?p=1 喜欢的朋友可 ...

  4. Java数据结构和算法 - 数组

    Q: 数组的创建? A: Java中有两种数据类型,基本类型和对象类型,在许多编程语言中(甚至面向对象语言C++),数组也是基本类型.但在Java中把数组当做对象来看.因此在创建数组时,必须使用new ...

  5. C语言 - 基础数据结构和算法 - 企业链表

    听黑马程序员教程<基础数据结构和算法 (C版本)>,照着老师所讲抄的, 视频地址https://www.bilibili.com/video/BV1vE411f7Jh?p=1 喜欢的朋友可 ...

  6. 数据结构与算法之链表-javascript实现

    链表的定义: 链表是一种物理存储单元上非连续.非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的.链表由一系列结点(链表中每一个元素称为结点)组成,结点可以在运行时动态生成.每个结点 ...

  7. Java数据结构和算法之链表

    三.链表 链结点 在链表中,每个数据项都被包含在‘点“中,一个点是某个类的对象,这个类可认叫做LINK.因为一个链表中有许多类似的链结点,所以有必要用一个不同于链表的类来表达链结点.每个LINK对象中 ...

  8. JavaScript 数据结构与算法3(链表)

    学习数据结构的 git 代码地址: https://gitee.com/zhangning187/js-data-structure-study 1.链表 本章学习如何实现和使用链表这种动态的数据结构 ...

  9. 数据结构和算法 – 8.链表

    8.1.数组存在的问题 在处理列表的时候数组是常用的数据结构.数组可以对所存储的数据项提供快速地存取访问,而且它很易于进行循环遍历操作.当然,数组已经是语言的一部分了,用户不需要使用额外的内存,也不需 ...

  10. JavaScript数据结构与算法(六) 链表的实现

    // 链表存储有序的元素集合,但不同于数组,链表中的元素在内存中并不是连续放置的.每个 // 元素由一个存储元素本身的节点和一个指向下一个元素的引用(也称指针或链接)组成.下图展 // 示了一个链表的 ...

随机推荐

  1. Scoop: 开发者多环境管理利器

    Scoop是什么? Scoop 是一个基于 Windows 的包管理器,能够帮助开发者高效管理开发环境和应用程序. 它推荐通过命令行进行包的安装.更新和卸载,同时提供了简单易用的包组织方式,透明化了安 ...

  2. Qt音视频开发8-ffmpeg保存裸流

    一.前言 最开始做的ffmpeg保存视频文件,就是直接保存的裸流数据,裸流数据一般是H264格式的数据,这种数据文件可以用部分播放器播放,由于不是标准的格式,很多播放器其实不支持的,需要安装对应的解码 ...

  3. vue.js中vue.config.js的配置说明

    如果你的项目没有vue.config.js,请在根目录新建一个. vue.config.js里面的代码如下: module.exports = { /** 区分打包环境与开发环境 * process. ...

  4. CDS标准视图:设备功能位置变更历史 I_EQUIPINSTALLATIONHISTORYC

    视图名称:I_EQUIPINSTALLATIONHISTORYC 视图类型:基础视图 视图代码: 点击查看代码 @EndUserText.label: 'Equipment Installation ...

  5. HTTP方法-GET对比POST

    什么是 HTTP ? 超文本传输协议(HTTP)的设计目的是保证客户端与服务器之间的通信. HTTP 的工作方式是客户端与服务器之间的请求-应答协议. web 浏览器可能是客户端,而计算机上的网络应用 ...

  6. 一文搞懂SaaS架构建设流程:业务战略设计、架构蓝图设计、领域系统架构设计、架构治理与实施

    大家好,我是汤师爷~ SaaS架构建设是一项复杂的系统工程,不仅需要技术层面的实现,更要从业务战略.架构设计.治理与实施等多个维度进行全面规划. 一个成功的SaaS架构可以帮助企业降低IT成本.提升业 ...

  7. Docker基础命令(安装和创建管理容器)

     docker ps -a 查看容器 docker inspect c008 使用 inspect 命令查看镜像详细信息,包括制作者.适应架构.各层的数字摘要等.

  8. Flu PG walkthrough Intermediate

    nmap ┌──(root㉿kali)-[/home/ftpuserr] └─# nmap -p- -A 192.168.192.41 Starting Nmap 7.94SVN ( https:// ...

  9. [BZOJ P2771] 天才ACM

    [BZOJ P2771] 天才ACM 传送门 朴素算法 枚举终点 \(r\),对区间 \([l, r]\) 排序求校验值 \(sum\),比较 \(sum\) 和 \(t\) $ sum \le t ...

  10. 一探究竟!天翼云2023MWC展区“亮”了!

    6月28日,2023MWC上海世界移动通信大会(简称"MWC上海")在上海新国际博览中心启幕.中国电信开设"数智万融 畅享未来"主题展区,从科技创新.安全筑防及 ...