数据结构 线性结构篇——动态数组和时间复杂度分析(3)

简介: 数据结构 线性结构篇——动态数组和时间复杂度分析

三、时间复杂度分析

3.1 基础

五种常见的时间复杂度

O(1):常数复杂度, 最快的算法,数组的存取是O(1)

O(n):线性复杂度, 例如:数组, 以遍历的方式在其中查找元素

O(logN):对数复杂度

O(nlogn):求两个数组的交集, 其中一个是有序数组,A数组每一个元素都要在B数组中进行查找操作,每次查找如果使用二分法则复杂度是 logN

O(n2):平方复杂度,求两个无序数组的交集

在这里,大O描述的是算法的运行时间和输入数据之间的关系


3.2 举例说明


大家可以看下面一个例子


public static int sum(int[] nums){
  int sum = 0;
  for(int num: nums) sum += num;
  return sum;
}

这个算法是 O(n) 复杂度的,其中 n是nums中的元素个数,算法和n呈线性关系


为什么说他是 O(n) 的时间复杂度呢,因为这个算法运行的时间的多少是和 nums 中元素的个数成线性关系的,那么这个线性关系,表现在我们的 n 是一次方,它不是 O(n) 方的,也不是 O(n) 的立方,n 对应的是一次方。


我们忽略常数,实际时间是: T = c1*n+c2

c1:我们要把这个数据从 nums 数组中取出来,其次我们还要把 sum 这个数取出来,然后 num 这个数和 sum 相加,最终呢我们要这个结果扔回给 sum 中

c2:开始开辟了一个Int型的空间,我们把它叫 sum ,要把 0 初始化赋值给sum,在最终呢我们还要把这个 sum 给 return 回去


一方面把 c1 和 c2 具体分析出来是不大必要的,另一方面也是不太可能的,为什么说不可能呢?如果说把 num 从 nums 中取出来,基于 不同的语言,基于 不同的实现,它实际运行的 时间是不等的,就算转换成机器码,它对应的机器码的指令数也有可能是不同的,就算是指令数是相同的,同样的一个指令,在我们 cpu 的底层,你使用的 cpu 不同,很有可能,执行的操作也是不同的,所以在实际上我们可能说出 c1 是几条指令,但是却很难说出 c1 到底是多少,c2也是同理,正因为如此,我们在进行时间复杂度时,是忽略这些常数的。


忽略这些常数就意味着什么,就意味着这些 t = 2*n +2 和 t=2000*n+10000算法这些都是 O(n) 的算法,见下面列表:换句话说他们都是线性数据的算法,也就是说我们这个算法消耗的时间是和我们输入数据的规模成一个线性相关的,t=1*n*n+0也线性算法是和我们成平方关系的 ,他的性能比上面的差,因为他是 O(n^2)的


算法 时间复杂度

T = 2*n + 2 O(n)

T = 2000*n + 10000 O(n)

T = 1nn + 0 O(n^2)

T = 2nn + 300n + 10 O(n^2)

O(n)和O(n^2)并不代表说 O(n)的算法快于 O(n^2)的算法,我们要看 n 的常数,比如 n=3000 的时候,或者 n>3000 的时候,O(n^2)消耗的时间是远远大于 O(n)的,n越大 O(n)远远快于 O(n^2)

O:描述的是一个算法,也就是说渐进时间复杂度

当高阶向和低阶项同时出现的时候,低阶项会被忽略,比如说:T = 2*n*n + 300n + 10当中 2*n*n,是O(n^2)级别的算法,属于高阶项,300n是O(n)的算法低阶项,当n无穷大的时候,低阶项起的作用很小。


3.3 分析动态数组的时间复杂度


3.3.1 添加操作


添加操作 总体来说属于 O(n) 级别的复杂度,如下列表


添加方法 时间复杂度

addLast(e) O(1)

addFirst(e) O(n)

add(index,e) O(n/2) = O(n)

在程序设计中,我们要采用最严谨的设计,需要考虑到最坏的情况,所以我们说添加操作时属于 O(n) 级别的复杂度,是因为我们在 addLast 的时候,有可能会进行 resize 的操作,我们从最坏的情况分析是对的,但是 addLast 不可能每次都是进行 resize 操作,比如 size 有十个,我们要添加十个元素后才会触发一个 resize ,我们要在添加十个元素才会触发一个 resize,因此我们使用最坏情况进行分析的是不合理的,那么分析 addLast 时间复杂度呢,请看下面小节。


3.3.2 resize的复杂度分析

image.png


总共进行了17次基本操作:9次添加操作8次元素转移操作


9次addLast操作,触发resize,总共进行了17次基本操作,平均每次addLast操作,进行了2次基本操作

假设 capacity = n,n+1 次addLast,触发resize,总共进行了2n+1次基本操作,平均,每次addLast操作,进行了2次基本操作

这样均摊计算,时间复杂度是O(1)的,在这个例子里,这样均摊计算,比计算最坏情况有意义

addLast 均摊复杂度是O(1)的,同理我们看 removeLast操作,均摊复杂度也是O(1)

3.3.3 复杂度震荡


当我们数组容量满了的时候,因为是动态数组,回去自动扩容,我们又马上去remove 一个操作的时候,因为数组容量小于 初始容量的一半的时候,又会 自动 resize缩减为一半的大小,如此操作,就会一个问题,就是我们在 removeLast的时候 resize 过于着急(Eager)

image.png


解决方案:Lazy,懒散的,其实也很简单,如下图所示:

image.png


当我们的 size == capacity /4 时候,才将capacity 减半,实现方式如下:

  //从数组中删除index位置的元素,返回删除的元素
    public E remove(int index){
        if(index < 0 || index >= size)
            throw new IllegalArgumentException("remove failed . Index is illegal.");
        E ret = data[index];
        for (int i = index+1 ; i < size; i++)
                data[i - 1] = data[i];
        size--;
        // loitering objects != memory leak
        data[size] = null;//如果一旦使用新的元素,添加新的对象就会覆盖掉
        if(size == data.length / 4 && data.length / 2 != 0)
            resize(data.length / 2);
        return ret;
    }

完整代码:

package com.bj.array;
/**
 * @program: Data-Structures
 * @ClassName Array
 * @description:
 * @author: lyy
 * @create: 2019-11-18 22:27
 * @Version 1.0
 **/
public class ArrayPlus<E> {
    private E[] data;
    private int size;
    //构造函数,传入数组的容量capacity 构造array
    public ArrayPlus(int capacity){
        data = (E[])new Object[capacity];
        size = 0;
    }
    //无参数的构造函数,传入数组的容量capacity=10
    public ArrayPlus(){
        this(10);
    }
    //获取元素中的个数
    public int getSize(){
        return size;
    }
    //获取数组的容量
    public int getCapacity(){
        return data.length;
    }
    //返回数组是否为空
    public boolean isEmpty(){
        return size == 0;
    }
    //在所有元素后添加一个新元素
    public void addLast(E e){
        add(size,e);
    }
    //想所有元素前添加一个新元素
    public void addFirst(E e){
        add(0,e);
    }
    //在第Index个位置插入一个新元素e
    public void add(int index,E e){
        if(index < 0 || index > size)
            throw new IllegalArgumentException("Add failed . Require index < 0 || index > size.");
        if(size == data.length)
            resize(2 * data.length);
        for (int i = size - 1; i >= index ; i--)
            data[i+1] = data[i];
        data[index]  = e;
        size++;
    }
    //获取index索引位置的元素
   public E get(int index){
        if(index < 0 || index >= size)
            throw new IllegalArgumentException("Get failed . Index is illegal.");
        return data[index];
    }
    //修改index索引位置的元素为e
    public void set(int index,E e){
        if(index < 0 || index >= size)
            throw new IllegalArgumentException("Set failed . Index is illegal.");
         data[index] = e;
    }
    //查找数组中是否有元素e
    public boolean contains(E e){
        for (int i = 0; i < size; i++) {
            if(data[i].equals(e))
                return true;
        }
        return false;
    }
    //查找数组中元素e所在的索引,如果不存在元素e,则返回-1
    public int find(E e){
        for (int i = 0; i < size; i++) {
            if(data[i].equals(e))
                return i;
        }
        return -1;
    }
    //从数组中删除index位置的元素,返回删除的元素
    public E remove(int index){
        if(index < 0 || index >= size)
            throw new IllegalArgumentException("remove failed . Index is illegal.");
        E ret = data[index];
        for (int i = index+1 ; i < size; i++)
                data[i - 1] = data[i];
        size--;
        // loitering objects != memory leak
        data[size] = null;//如果一旦使用新的元素,添加新的对象就会覆盖掉
        if(size == data.length / 4 && data.length / 2 != 0)
            resize(data.length / 2);
        return ret;
    }
    //从数组中删除第一个位置的元素,返回删除的元素
    public E removeFirst(){
        return remove(0);
    }
    //从数组中删除最后一个位置的元素,返回删除的元素
    public E removeLast(){
        return remove(size-1);
    }
    //从数组中删除元素e
    //思考?如果返回是否删除成功 2、如果存在重复数据,如何删除多个
    public void removeElement(E e){
        int index = find(e);
        if(index != -1)
            remove(index);
    }
    @Override
    public String toString(){
        StringBuffer res = new StringBuffer();
        res.append(String.format("Array:Size = %d,capacity = %d\n",size,data.length));
        res.append("[");
        for (int i = 0; i < size; i++) {
            res.append(data[i]);
            if(i != size - 1)
                res.append(",");
        }
        res.append("]");
        return res.toString();
    }
    private void resize(int newCapacity) {
        E[] newData = (E[])new Object[newCapacity];
        for (int i = 0; i < size; i++)
            newData[i] = data[i];
        data = newData;
    }
}
目录
相关文章
|
2天前
|
存储 搜索推荐 算法
【数据结构】树型结构详解 + 堆的实现(c语言)(附源码)
本文介绍了树和二叉树的基本概念及结构,重点讲解了堆这一重要的数据结构。堆是一种特殊的完全二叉树,常用于实现优先队列和高效的排序算法(如堆排序)。文章详细描述了堆的性质、存储方式及其实现方法,包括插入、删除和取堆顶数据等操作的具体实现。通过这些内容,读者可以全面了解堆的原理和应用。
32 16
|
25天前
|
机器学习/深度学习 存储 缓存
数据结构与算法学习十:排序算法介绍、时间频度、时间复杂度、常用时间复杂度介绍
文章主要介绍了排序算法的分类、时间复杂度的概念和计算方法,以及常见的时间复杂度级别,并简单提及了空间复杂度。
19 1
数据结构与算法学习十:排序算法介绍、时间频度、时间复杂度、常用时间复杂度介绍
|
2月前
|
存储 Java
java数据结构,线性表链式存储(单链表)的实现
文章讲解了单链表的基本概念和Java实现,包括头指针、尾节点和节点结构。提供了实现代码,包括数据结构、接口定义和具体实现类。通过测试代码演示了单链表的基本操作,如添加、删除、更新和查找元素,并总结了操作的时间复杂度。
java数据结构,线性表链式存储(单链表)的实现
|
1月前
|
存储 编译器 C++
【初阶数据结构】掌握二叉树遍历技巧与信息求解:深入解析四种遍历方法及树的结构与统计分析
【初阶数据结构】掌握二叉树遍历技巧与信息求解:深入解析四种遍历方法及树的结构与统计分析
|
2月前
|
机器学习/深度学习 算法 Java
[算法与数据结构] 谈谈线性查找法~
该文章详细介绍了线性查找法的基本概念与实现方法,通过Java代码示例解释了如何在一个数组中查找特定元素,并分析了该算法的时间复杂度。
|
20天前
|
算法
[数据结构] -- 时间复杂度和空间复杂度
[数据结构] -- 时间复杂度和空间复杂度
13 0
|
24天前
探索顺序结构:栈的实现方式
探索顺序结构:栈的实现方式
|
29天前
|
存储 算法
【数据结构】二叉树——顺序结构——堆及其实现
【数据结构】二叉树——顺序结构——堆及其实现
|
2月前
|
存储 算法 C语言
数据结构基础详解(C语言): 二叉树的遍历_线索二叉树_树的存储结构_树与森林详解
本文从二叉树遍历入手,详细介绍了先序、中序和后序遍历方法,并探讨了如何构建二叉树及线索二叉树的概念。接着,文章讲解了树和森林的存储结构,特别是如何将树与森林转换为二叉树形式,以便利用二叉树的遍历方法。最后,讨论了树和森林的遍历算法,包括先根、后根和层次遍历。通过这些内容,读者可以全面了解二叉树及其相关概念。
|
2月前
|
存储 机器学习/深度学习 C语言
数据结构基础详解(C语言): 树与二叉树的基本类型与存储结构详解
本文介绍了树和二叉树的基本概念及性质。树是由节点组成的层次结构,其中节点的度为其分支数量,树的度为树中最大节点度数。二叉树是一种特殊的树,其节点最多有两个子节点,具有多种性质,如叶子节点数与度为2的节点数之间的关系。此外,还介绍了二叉树的不同形态,包括满二叉树、完全二叉树、二叉排序树和平衡二叉树,并探讨了二叉树的顺序存储和链式存储结构。