数据结构与算法之美(数组 栈和队列)

数据结构与算法之美(数组)

一、如何实现随机访问?

数组是什么

数组(Array)是一种线性表 数据结构。它用一组连续的内存空间 ,来存储一组具有相同类型的数据。

关键词一: 线性表
顾名思义,线性表就是数据排成像一条线一样的结构。每个线性表上的数据最多只有前和后两个方向。其实除了数组,链表、队列、栈等也是线性表结构。

在这里插入图片描述
而与它相对立的概念是非线性表,比如二叉树、堆、图等。之所以叫非线性,是因为,在非线性表中,数据之间并不是简单的前后关系

在这里插入图片描述
关键词二:连续的内存空间和相同类型的数据

正是因为这两个限制,数组才具有了随机访问特性。也正是因为这两个限制,使得数组的删除、插入操作变得低效。为了保证数据的连续性,比如,容量为 20,数组中的数据 1 - 15,若是删除第 1 个数据,那么后面的 14 个数据下标全部向前移动一位,若是在第 1 位插入数据,那么后面的 15 个数据下标全部向后移动一位。

回到最初的问题 数组是如何随机访问的

举例:一个长度为 10 的 int 类型的数组 int[] a = new int[10] 来举例。在我画的这个图中,计算机给数组 a[10],分配了一块连续内存空间 1000~1039,其中,内存块的首地址为 base_address = 1000。
在这里插入图片描述

data_type_size 表示数组中每个元素的大小。数组中存储的是 int 类型数据,所以 data_type_size 就为 4 个字节。

一维数组寻址公式:

a[i]_address = base_address + i * data_type_size

这里的话 如果我们把数组从 1 开始计数,那我们计算数组元素 a[k]的内存地址就会变为:

a[i]_address = base_address + ( i - 1 ) data_type_size

以上就是 为什么大多数编程语言中,数组要从0开始编号,而不是从1开始呢? 因为从1开始 cpu会多执行一次 减法操作

知道寻址公示后就好获取你想获取的地址了 这样数组就可以通过下标 进行随机访问了

注意:

链表适合插入、删除,时间复杂度O(1);数组是适合查找操作,但是查找的时间复杂度并不为O(1)。【数组支持随机访问,根据下标随机访问的时间复杂度为O(1)。

二、低效的“插入”和“删除”

先看看插入操作:

如果在数组的末尾插入新的数据,时间复杂度恰好为O(1),如果在数组的开头插入一个元素,时间复杂度就是O(n).因为每个位置的插入概率是相同的,所以平均时间复杂度是(1+2+…n)/n=O(n)

对于删除操作:

如果删除第K个位置的数据,为了保证内存的连续性,也需要搬移数据,保证内存的连续性,和插入类似,如果删除数组末尾的数据,则最好情况时间复杂度为O(1);如果删除开头的数据,则**最坏情况时间复杂度为O(n);**平均情况时间复杂度也为O(n)。

实际上,在某些特定的场景下,并不需要追求数组的连续性,每次删除操作就是对要删除的数据做标定,可以将多次删除操作集中在一起执行,类似于JVM中的标记-清除算法,这样可以提高删除的效率。

这个就是标记法 好像 计算机硬盘的原理也是类似的 删除数据其实并没有删除 只是在一个表上显示这个地方是空了 其实这个地方还是又数据的 只是下次输入数据 就把这个地方覆盖了

三、警惕数组的访问越界问题

int main(int argc, char* argv[]){
    
    
    int arr[3] = {
    
    0};
    for( int i = 0; i<=3; i++){
    
    
        arr[i] = 0;
        printf("ccw\n");
    }
    return 0;
}

因为,数组大小为3,a[0],a[1],a[2],而我们的代码因为书写错误,导致for循环的结束条件错写为了i<=3而非i<3,所以当i=3时,数组a[3]访问越界。

四、容器能否完全替代数组

针对数组类型,很多编程语言都提供了容器类,比如C++ STL 的vector,java 的ArrayList,容器最大的优势是将数组的操作封装起来了,比如数组的删除和插入操作,还有就是容器支持动态扩容。当然数组也有其适合使用的场景。
数组适合的场景:
1) Java ArrayList 的使用涉及装箱拆箱,有一定的性能损耗,如果特别在乎性能,可以考虑数组;
2) 若数据大小事先已知,并且涉及的数据操作非常简单,可以使用数组;
3) 表示多维数组时,数组往往更加直观;
4) 业务开发容器即可。底层开发,如网络框架,性能优化等,选择数组。

其中 java的ArrayList 支持动态扩容。 这个具体可以看我之前写的博客

数据结构与算法之美(栈)

一、栈是什么

它是一种线性表,同为线性表还有楼上的数组。
栈的操作受限,具体表现在先进后出,后进先出,只能在一端进行数据的插入和删除。

如图:

**在这里插入图片描述**

二、为什么需要栈?

栈是一种操作受限的数据结构,其操作特性用数组链表均可实现。

不过任何数据结构都是对特定应用场景的抽象,数组和链表虽然使用起来更加灵活,但却暴露了几乎所有的操作,难免会引发错误操作的风险。所以,当某个数据集合只涉及在某端插入和删除数据,且满足后进者先出,先进者后出的操作特性时,我们应该首选栈这种数据结构。

三、如何实现栈

可以用数组实现 当然也可以用链表。(进栈,出栈,判断栈是否为空,判断栈是否满了)
这个就不丢代码了 很简单 大家可以自行百度

四、栈的应用

1)栈在表达式求值

编译器就是通过两个栈来实现的。其中一个保存操作数的栈,另一个是保存运算符的栈。我们从左向右遍历表达式,当遇到数字,我们就直接压入操作数栈;当遇到运算符,就与运算符栈的栈顶元素进行比较。
在这里插入图片描述

2)栈在括号匹配中的应用

(比如:{}{ ()})
用栈保存为匹配的左括号,从左到右一次扫描字符串,当扫描到左括号时,则将其压入栈中;当扫描到右括号时,从栈顶取出一个左括号,如果能匹配上,则继续扫描剩下的字符串。如果扫描过程中,遇到不能配对的右括号,或者栈中没有数据,则说明为非法格式。

当所有的括号都扫描完成之后,如果栈为空,则说明字符串为合法格式;否则,说明未匹配的左括号为非法格式。

3)实现浏览器的前进后退功能

我们使用两个栈X和Y,我们把首次浏览的页面依次压如栈X,当点击后退按钮时,再依次从栈X中出栈,并将出栈的数据一次放入Y栈。当点击前进按钮时,我们依次从栈Y中取出数据,放入栈X中。当栈X中没有数据时,说明没有页面可以继续后退浏览了。当Y栈没有数据,那就说明没有页面可以点击前进浏览了。
在这里插入图片描述
回退一次
在这里插入图片描述

4)栈在函数调用中的应用

操作系统给每个线程分配了一块独立的内存空间,这块内存被组织成“栈”这种结构,用来存储函数调用时的临时变量。每进入一个函数,就会将临时变量作为一个栈帧入栈,当被调用函数执行完成,返回之后,将这个函数对应的栈帧出栈。

推荐刷题:
力扣的 20,155,232,844
20. 有效的括号

class Solution {
    
    
public:
    bool isValid(string s) {
    
    
        stack<int> st;
        for (int i = 0; i < s.size(); i++) {
    
    
            if (s[i] == '(') st.push(')');
            else if (s[i] == '{') st.push('}');
            else if (s[i] == '[') st.push(']');
            else if (st.empty() || st.top() != s[i]) return false;
            else st.pop(); 
        }
        return st.empty();
    }
};

数据结构与算法之美(队列)

一、什么是队列?

队列:先进者先出,这就是典型的“队列”结构。队列支持两个操作:入队enqueue(),放一个数据到队尾;出队dequeue(),从队头取一个元素。正因为这样,所以,和栈一样,队列也是一种操作受限的线性表。

二、实现队列的几种方式

数组:

public class ArrayQueue {
    
    
    // 数组items
    private String[] items;
    // 数组大小
    private int n = 0;
    // 数组头下标
    private int head = 0;
    // 数组尾下标
    private int tail = 0;

    public ArrayQueue(int capacity) {
    
    
        items = new String[capacity];
        n = capacity;
    }
    /**
     * 入队
     */
    public boolean enqueue(String ele) {
    
    
        // 队列满了
        if (tail == n) {
    
    
            return false;
        }
        items[tail++] = ele;
        return true;
    }
    /**
     * 出队
     */
    public String dequeue() {
    
    
        if (head == tail) {
    
    
            return null;
        }
        return items[head++];
    }
    /**
     * 打印所有队列元素
     */
    public void printAll() {
    
    
        for (int i = head; i < tail; i++) {
    
    
            System.out.print(items[i] + " ");
        }
        System.out.println();
    }
}

链表:



public class LinkedQueue {
    
    
    // 头指针
    private Node head = null;
    // 尾指针
    private Node tail = null;

    /**
     * 入队
     */
    public void enqueue(String ele) {
    
    
        if (head == null) {
    
    
            Node node = new Node(ele, null);
            head = node;
            tail = node;
        } else {
    
    
            tail.next = new Node(ele, null);
            tail = tail.next;
        }
    }
    /**
     * 出队
     */
    public String dequeue() {
    
    
        if (head == null) {
    
    
            return null;
        }
        String ret = head.data;
        head = head.next;

        // 如果出队后,头指针head为null,则同时置tail为null
        if (head == null) {
    
    
            tail = null;
        }
        return ret;
    }
    public void printAll() {
    
    
        Node p = head;

        if (p != null) {
    
    
            System.out.print(p.data + " ");
            p = p.next;
        }
        System.out.println();
    }
    private static class Node {
    
    
        private String data;
        private Node next;

        public Node(String data, Node next) {
    
    
            this.data = data;
            this.next = next;
        }
        public String getData() {
    
    
            return data;
        }
    }
}

三、 顺序队列和链式队列的不同点

顺序队列使用数组实现,所以有数组大小的限制,在数据结构中有对应的属性n

顺序队列队空条件为:head == tail; 队满条件为:tail == n。

链式队列队空条件为:head == null; 队满条件要取决于队列的长度 (tail+1)%n=head。

四、队列有哪些常见的应用?

1.阻塞队列

1)在队列的基础上增加阻塞操作,就成了阻塞队列。
2)阻塞队列就是在队列为空的时候,从队头取数据会被阻塞,因为此时还没有数据可取,直到队列中有了数据才能返回;如果队列已经满了,那么插入数据的操作就会被阻塞,直到队列中有空闲位置后再插入数据,然后在返回。

2.并发队列

1)在多线程的情况下,会有多个线程同时操作队列,这时就会存在线程安全问题。能够有效解决线程安全问题的队列就称为并发队列。
2)并发队列简单的实现就是在enqueue()、dequeue()方法上加锁,但是锁粒度大并发度会比较低,同一时刻仅允许一个存或取操作。
3)实际上,基于数组的循环队列利用CAS原子操作,可以实现非常高效的并发队列。这也是循环队列比链式队列应用更加广泛的原因。

推荐题目:
力扣的 232 225

猜你喜欢

转载自blog.csdn.net/qq_54729417/article/details/122850893