基本数据结构

数组

定义:数组是种线性表数据结构,他用一组连续的内存空间,来存储一组具有相同类型的数据。对内存的要求比较高

首先是线性表:每个数据只有前后俩个方向

连续的内存空间和相同的数据类型:可以支持下标随机访问。

插入操作:O(n),由于涉及到后续数组元素的迁移

如果是无序的话,假设现在要向 数组a中第三个元素后插入一个数据,我们可以直接将第四个元素一直数组末尾,然后将数据放入第四个元素,时间复杂度为O(1)

删除操作:O(n) ,但删除一个元素后,后续的元素需要进行数据的搬移。

优化:我们可以先将该位置的元素标记为已删除,当数组的空间不够时,可以触发一次真正的操作,这样就减少了数组的搬移。java中的标记回收垃圾算法于此类似。

链表

定义:通过指针将零散的内存块串联起来使用。对内存要求比较低。

节点:内存块

后继指针next:记录下一个节点的位置

头结点:链表的第一个节点

尾节点:链表的最后一个节点

单链表:每个节点有一个next节点,尾节点指向null。复杂度:查询O(n ) , 删除:O(1),插入O(1)

循环链表:在单链表的基础上,尾节点不指向null ,而是指向头结点

双向链表:在单链表的的基础上,每个节点加了pre 节点,指向上一个节点

常用的算法以及解决:

回文字符串:使用快慢两个指针找到链表中点,慢指针每次前进一步,快指针每次前进两步。在慢指针前进的过程中,同时修改其 next 指针,使得链表前半部分反序。最后比较中点两侧的链表是否相等。

所有的数据结构都可以归结为俩个数据结构,一个是数组,一个是链表

 

 列数组列表
优势随机访问只能顺序访问
劣势需要连续的空间需要的空间可以不连续

 

定义:栈主要包含两个操作, 入栈和出栈, 也就是在栈顶插入一个数据和从栈顶删除一个数据。先进后出

出栈与入栈的复杂度均为O(1)比如
 

应用

函数调用栈,用来存储函数的参数以及返回值等

求运算表达式:

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


如果比运算符栈顶元素的优先级高, 就将当前运算符压入栈; 如果比运算符栈顶元素的优先级低或者相同, 从运算符栈中取栈顶运算符, 从操作数栈的栈顶取 2 个操作数, 然后进行计算, 再把计算完的结果压入操作数栈, 继续比较。

用来判断()表达式是否正确。

比如, {[{}]}或 [{()}([])] 等都为合法格式, 而{[}()] 或 [({)] 为不合法的格式。 那我现在给你一个包含三种括号的表达式字符串, 如何检查它是否合法呢?


这里也可以用栈来解决。 我们用栈来保存未匹配的左括号, 从左到右依次扫描字符串。 当扫描到左括号时, 则将其压入栈中; 当扫描到右括号时, 从栈顶取出一个左括号。 如果能够匹配, 比如“(”跟“)”匹配, “[”跟“]”匹配, “{”跟“}”匹配, 则继续扫描剩下的字符串。 如果扫描的过程中, 遇到不能配对的右括号, 或者栈中没有数据, 则说明为非法格式。


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

实现

public class ArrayStack {

    private String [] items;

    private int count;  //栈内数量

    private int size;  

    public ArrayStack(int size){
        items = new String[size];
        count = 0;
        this.size = size;
    }

    public boolean push(String param){
        if(size == count){
           return false;
        }
        items[count++] = param;
        return true;
    }

    public String pop(){
        if(count ==0){
            return null;
        }
        return  items[--count];
    }

}

队列:

定义:队列跟栈一样, 也是一种抽象的数据结构。 它具有先进先出的特性, 支持在队尾插入元素, 在队头删除元素

实现:

public class ArrayQueue<T> {

    private Object[] items;

    private int tail ;

    private int head;

    private int size;

    public ArrayQueue(int size){
        this.size = size;
        tail = head = 0 ;
        items = new Object[size];
    }

    public boolean enqueue(T t){
        //队列满
        if(tail == size && head == 0){
            return false;
        }
        //由于不断的进行入队和出队,导致head 一直向后移,当tail 指向末尾时 , 此时需要进行数据的迁移
        if(tail == size){

            for (int i=head;i<=tail;i++){
                items[i-head] = items[i];
            }

            tail = tail - head;
            head = 0 ;
        }


        items[tail++] = t;
        return true;
    }

    public T dequeue(){
        if(tail == head){
            return null;
        }
        return (T) items[head++];
    }
}

循环队列:

重点是判空与队列是否满,判空与上述一样是 tail == head, 判断是否满:(tail + 1)%n =head; 会浪费一个空间。。

 public boolean enqueue(T t){
        //队列满
        if((tail+1)% size == head){
            return false;
        }
        items[tail]= t;
        tail = (tail+1) %size ;
        return true;
    }

    public T dequeue(){
        if(tail == head){
            return null;
        }
        Object item = items[head];
        head = (head+1) %size;
        return (T) item;
    }

并发队列和阻塞队列。

应用:

线程池,对象池,连接池

跳表:

对于一个单链表来讲, 即便链表中存储的数据是有序的, 如果我们要想在其中查找某个数据, 也只能从头到尾遍历链表。 这样查找效率就会很低, 时间复杂度会很高, 是 O(n)
 

可以实现快速查找,插入和删除。

分析:

空间复杂度为O(n)

查询、删除、添加的时间复杂度为O(logn)

问题:当我们不停的向跳表中插入数据是,如果不更新索引,就有可能出现2个索引节点之间的数据非常多,甚至可能会退化成单链表。

当我们往跳表中插入数据的时候,我们可以选择同时将这个数据插入到部分索引层中。如何选择加入哪些索引层呢?

我们通过一个随机函数,来决定将这个结点插入到哪几级索引中,比如随机函数生成了值 K,那我们就将这个结点添加到第一级到第 K 级这 K 级索引中。

散列表

散列表用的是数组支持按照下标随机访问数据的特性, 所以散列表其实就是数组的一种扩展, 由数组演化而来。 可以说, 如果没有数组, 就没有散列表。一般是通过对key 来进行hash,然后通过特定的算法 根据hash找到对应的下标,然后放入。

散列函数设计原则:

1 经过hash后得到的值为非负数,因为数组的的下标为非负数

2 如果key1 == key1 ,那么hash(key1) == hash(key2)

3 如果key1 != key2 , 那么hash(key1) != hash(key2)   //这个很难保证
 

散列冲突解决方法:

1 开放寻址法:如果经过计算得到的hash值对应的下标已经有数据了,我们可以重新探测一个位置。

       探测位置的方法:

直接向后找,如果有空的,就插入。

二次探测 : 跟线性探测很像, 线性探测每次探测的步长是 1, 那它探测的下标序列就是hash(key)+0, hash(key)+1, hash(key)+2……而二次探测探测的步长就变成了原来的“二次方”, 也就是说, 它探测的下标序列就是 hash(key)+0, hash(key)+1*1 , hash(key)+2*2 ……

双重散列:使用多个hash函数,如果第一个哈希函数计算得到的已经有值,那么再使用第二个,知道找到符合条件的

但是使用了开放寻址法,删除就不能直接删除了,直接删除会对查询造成影响,我们只可以对该元素标记为已删除。

适用于装载因子很小,数据量很小的场景,java 的 threadLocalMap 使用的就是该方法

2 拉链法 :将数组中每个下标对应的元素都设置为链表,如果有冲突,就放入链表。优化:可以使用跳表,红黑树等数据结构来代替链表,即使最后所有数据全放在一个bucket中,查询效率优势logN,而不是O(n)。java中的HashMap使用的就是拉链法,当链表中元素个数大于8的时候,就会转化为红黑树,小于8的时候,红黑树会转化为链表

降低扩容效率:

如果负载因子过大的话,此时需要进行扩容,使用一次性扩容,会导致某次插入变得很慢,我们可以使用分批次的扩容。

当装载因子触达阈值之后, 我们只申请新空间, 但并不将老的数据搬移到新散列表中。当有新数据要插入时, 我们将新数据插入新散列表中, 并且从老的散列表中拿出一个数据放入到新散列表。 每次插入一个数据到散列表, 我们都重复上面的过程。 经过多次插入操作之后, 老的散列表中的数据就一点一点全部搬移到新散列表中了。 这样没有了集中的一次性数据搬移, 插入操作就都变得很快了。
查询的话,可以先去新的哈希表中进行查询,查不到的话,在去老的中查。

散列表这种数据结构虽然支持非常高效的数据插入、删除、查找操作,但是散列表中的数据都是通过散列函数打乱之后无规律存储的。也就说,它无法支持按照某种顺序快速地遍历数据。如果希望按照顺序遍历散列表中的数据,那我们需要将散列表中的数据拷贝到数组中,然后排序,再遍历。如果我们希望散列表可以有某种顺序,那么可以将散列表和链表结合起来。例如:java中的LinkedHashMap ,就是利用双向链表和散列表组合得。redis有序集合使用的是跳表和散列表结合

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值