重生之数据结构与算法----数组&链表

简介

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

数组

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

静态数组

  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]) + 1
                newNode.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();
    }
}
posted @ 2025-03-03 13:31  叫我安不理  阅读(577)  评论(1)    收藏  举报