线性数据结构 Linear

Linear

数组概述

数组有四种操作:添加、删除、更改和搜索。

读取是数组中最容易、最快的操作,因为数组以顺序的方式存储在内存中。
数组最突出的能力叫做随机读取,即通过一个下标读取值。

int a[] = {3, 1, 2, 5, 4, 9, 2, 7};
printf("%d", a[3]);

读取和更新时间复杂度\(O(1)\)

插入元素存在三种情况

  • 尾部插入
    只需将元素放在数组的末尾即可,等同于更新元素

  • 中间插入
    需要将插入位置和之后的元素向后移动

    for(int i = size - 1; i >= index; ++i)
        array[i + 1] = array[i];
    array[index] = element;
    ++size;
    
  • 超范围插入
    if (index > size || index < 0) throw new Error();

如果数组容量不够可以扩容,一般来说采用\(\times 2\)的方法

int[] arr = new int[16];
int newArr = new int[arr.Length * 2];
Array.Copy(arr, 0, newArr, 0, arr.Length);
arr = newArr;

删除元素插入元素操作过程相反,删除的元素其后的元素都向前挪动一位,因为不用考虑扩容,其方法比插入更简单

for(int i = index; i != size -1; ++i)
    array[i] = array[i+1];
--size;
// array[size] = default(int);

扩容、插入、删除的时间复杂度都为\(O(n)\)

数组的优劣

  • 高效的随机访问能力,通过下标可以在常数时间内找到对应元素,二分查找算法就利用了这一优势
  • 元素紧密的排列在内存中,扩容、插入和删除导致的大量移动都会影响效率

链表概述

链表是一种在物理上非连续的数据结构,有若干个节点组成,每个节点一级一级传递,分单向节点双向节点,下面以单向节点为例

template<typename T>
struct Node { // single linked node
    T val;
    Node* next;
}
template<typename T>
struct Binode { // double linked node
    T val;
    Binode* next;
    Binode* prev;
}

查找

Node *p = head; // 使用不存储数据的头节点
while (p.next != nullptr)
    if (p->next->val == search_value)
        return p; // 返回前一个节点
    else
        p = p->next;
return nullptr;

插入

如果是不存储数据的头节点,在中部和头部插入方法相同;
如果使用头指针,头节点存储数据,则不同

// insert to last
Node *p = head;
while (p.next) p = p->next;
p->next = new Node(val, nullptr);

// insert to position i
// assert(i <= size);
Node *p = head;
for (int i = 0; i != index; ++i) p = p->next;
p->next = new Node(val, p->next);

删除

Node *current;
Node *prev = head;
while ((current = prev->next) != nullptr)
    if (current->val == delete_value) break;
    else prev = current;
if (current) {
    prev->next = current->next;
    delete current;
}

查找的时间复杂度为\(O(n)\),如果不考虑插入和删除操作之前的查找,两个操作点时间复杂度都为\(O(n)\)

头指针和头节点
使用头指针则可在第一个节点存储数据,使用头节点可以免去对第一个节点的判断和对头指针的修改,在单向链表中会使用头节点使元素的插入和删除变简单

小结

查找 更新 插入 删除
数组 \(O(1)\) \(O(1)\) \(O(n)\) \(O(n)\)
链表 \(O(n)\) \(O(1)\) \(O(1)\) \(O(1)\)

栈和队列概述

栈(Stack)和队列(Queue)是计算机中常用的两种数据结构,是操作受限的线性表。

  • 栈的插入和删除等操作都在栈顶进行,它是先进后出(First In Last Out, FILO)的线性表。
  • 队列的删除操作在队头进行,而插入、查找等操作在队尾进行,它是先进先出(First In First Out, FIFO)的线性表。
栈的操作
  • 入栈(push)

  • 出栈(pop)

由于代码实现比较简单,不再展示代码

由于入栈和出栈只涉及最后一个元素,不涉及其他元素的整体移动,所以无论是以数组实现还是以链表实现,它们的时间复杂度都为O(1)

队列的操作

用数组实现时,为了入队操作方便,将最后入队元素的下一个位置规定为队尾(rear)

  • 入队
    将新元素放入队列中,只允许在队尾(rear)放入元素,新元素的下一个位置成为新的队尾
  • 出队
    把元素移出队列,只允许在队头(front)移除元素,出队元素后的下一个元素会成为新的队头

对于链表实现方式,队列的入队和出队与栈是大同小异的,而数组实现方式则采用循环队列的方式。即利用已出队元素留下的空间的,让队尾指针重新回到数组的首位

此时判断队满的条件为front == (rear + 1) % array.Length,判断队空的条件是rear == front,注意此时队尾指向的位置永远空出一位,否则空和满的条件就相同了,所以队列的最大容量比数组长度小1

入队和出队的时间复杂度同样为O(1)

栈和队列的应用

栈和队列都建立在数组和链表的基础上,但却限制了几种操作。正是由于它们的实用性

栈的输入顺序和输出顺序相反,所以栈的本质就是“对历史的回溯”

  • 递归就是建立在栈上的,先被调用的函数要等后被调用的函数结束才能结束。由此,我们总能用栈重写递归为迭代

  • 倒序字符串

  • 括号匹配

    Stack<char> stack = new Stack<char>();
    stack.Push('?');
    foreach (char c in s)
    {
        switch (c)
        {
            case '(':
                stack.Push('(');
                break;
            case '{':
                stack.Push('{');
                break;
            case '[':
                stack.Push('[');
                break;
            case ')':
                if (stack.Pop() != '(')
                    return false;
                break;
            case '}':
                if (stack.Pop() != '{')
                    return false;
                break;
            case ']':
                if (stack.Pop() != '[')
                    return false;
                break;
        }
    }
    return stack.Count == 1;
    
  • 表达式求值

队列的输入顺序和输出顺序相同,所以队列的本质就是“对历史的重播”

  • 多线程中,竞争锁的队列就是按照队列的顺序来决定线程的顺序的

双端队列

将栈和队列结合起来的结构叫做双端队列(deque),既可以在队头出队和入队,也可以在队尾出队和入队,虽然它的灵活性提高了,但在实际运用中不如栈和队列常见。

posted @ 2022-10-27 20:28  Violeshnv  阅读(34)  评论(0编辑  收藏  举报