【数据结构】初识数据结构

目录

1.前言

2.时间和空间复杂度

2.1算法效率

2.2时间复杂度

2.2.1概念

2.2.2大O渐进表示法

2.2.3推导大O阶方法

2.2.4常见时间复杂度计算举例

 2.3空间复杂度

2.3.1概念

2.3.2常见空间复杂度计算举例

3.包装类

3.1基本数据类型和对应的包装类

3.2装箱和拆箱

4.泛型

4.1概念

4.2引出泛型

4.3语法

5.泛型类的使用

5.1语法

5.2类型推导

 6.泛型的上界

6.1概念

6.2语法

7.泛型方法

8.总结


1.前言

本篇将主要介绍数据结构的基本知识:时间和空间复杂度、算法效率、大O渐进表示法、包装类、泛型相关知识。

2.时间和空间复杂度

2.1算法效率

算法效率分析分为两种:第一种是 时间效率 ,第二种是 空间效率
时间效率被称为时间复杂度。
空间效率被称作空间复杂度。
时间复杂度主要衡量的是一个算法的运行速度,而空间复杂度主要衡量一个算法所需要的额外空间。

2.2时间复杂度

2.2.1概念

一个算法所花费的时间与其中语句的执行次数成正比例,算法中的基本操作的执行次数,为算法的时间复杂度。

2.2.2大O渐进表示法

举个例子:请计算一下func1基本操作执行了多少次?

void func1(int N) {
        int count = 0;
        for (int i = 0; i < N; i++) {
            for (int j = 0; j < N; j++) {
                count++;
            }
        }
        for (int k = 0; k < 2 * N; k++) {
            count++;
        }
        int M = 10;
        while ((M--) > 0) {
            count++;
        }
        System.out.println(count);
    }

Func1 执行的基本操作次数 :

  • N = 10 F(N) = 130
  • N = 100 F(N) = 10210
  • N = 1000 F(N) = 1002010
实际中我们计算时间复杂度时,我们其实并不一定要计算精确的执行次数,而只需要 大概执行次数,那么这里我们 使用大 O 的渐进表示法(大O 符号( Big O notation) :是用于描述函数渐进行为的数学符号)

2.2.3推导大O阶方法

1 、用常数 1 取代运行时间中的所有加法常数。
2 、在修改后的运行次数函数中,只保留最高阶项。
3 、如果最高阶项存在且不是 1 ,则去除与这个项目相乘的常数。得到的结果就是大 O 阶。

 使用大O的渐进表示法以后,Func1的时间复杂度为:

N = 10 F(N) = 100
N = 100 F(N) = 10000
N = 1000 F(N) = 1000000
通过上面我们会发现大O阶的渐进表示法去掉了那些对结果影响不大的项,简洁明了的表示出了执行的次数。

另外,有些算法的时间复杂度存在最好、平均和最坏情况:

最坏情况:任意输入规模的最大运行次数(上界)
平均情况:任意输入规模的期望运行次数
最好情况:任意输入规模的最小运行次数(下界)

例如:在一个长度为N数组中搜索一个数据x
最好情况: 1 次找到
最坏情况: N 次找到
平均情况: N/2 次找到
在实际中一般情况关注的是算法的最坏运行情况,所以数组中搜索数据时间复杂度为 O(N).

2.2.4常见时间复杂度计算举例

例1:计算func2的时间复杂度?

    void func2(int N) {
        int count = 0;
        for (int k = 0; k < 2 * N; k++) {
            count++;
        }
        int M = 10;
        while ((M--) > 0) {
            count++;
        }
        System.out.println(count);
    }

1基本操作执行了2N+10次,通过推导大O阶方法知道,它的时间复杂度为 O(N)。

例2:计算func3的时间复杂度? 

    void func3(int N, int M) {
        int count = 0;
        for (int k = 0; k < M; k++) {
            count++;
        }
        for (int k = 0; k < N; k++) {
            count++;
        }
        System.out.println(count);
    }
2 基本操作执行了 M+N 次,有两个未知数 M N ,时间复杂度为 O(N+M)
例3:计算func4的时间复杂度?
void func4(int N) {
        int count = 0;
        for (int k = 0; k < 100; k++) {
            count++;
        }
        System.out.println(count);
    }
3 基本操作执行了 100 次,通过推导大 O 阶方法,时间复杂度为 O(1)。
例4:计算bubbleSort的时间复杂度?
void bubbleSort(int[] array) {
        for (int end = array.length; end > 0; end--) {
            boolean sorted = true;
            for (int i = 1; i < end; i++) {
                if (array[i - 1] > array[i]) {
                    Swap(array, i - 1, i);
                    sorted = false;
                }
            }
            if (sorted == true) {
                break;
            }
        }
    }
4 基本操作执行最好 N 次,最坏执行了 (N*(N-1))/2 次,通过推导大 O 阶方法 + 时间复杂度一般看最坏,时间复杂度为 O(N^2)。
例5:计算binarySearch的时间复杂度?
int binarySearch(int[] array, int value) {
        int begin = 0;
        int end = array.length - 1;
        while (begin <= end) {
            int mid = begin + ((end - begin) / 2);
            if (array[mid] < value)
                begin = mid + 1;
            else if (array[mid] > value)
                end = mid - 1;
            else
                return mid;
        }
        return -1;
    }

例5基本操作最好执行1次,最坏log₂N次,因为二分查找每次都会排除掉一半的不适合值,则每一次值都为上一次的1/2,所以当存在N个数据时,有总个数N/(2^(砍一半的次数)y) = 1(最后剩下的一个数) ==> y = log₂N,则时间复杂度为O(log₂N)。

例6: 计算阶乘递归factorial的时间复杂度?
long factorial(int N) {
        return N < 2 ? N : factorial(N-1) * N;
    }

例6通过计算分析发现基本操作递归了N次,且递归的时间复杂度=递归次数*每次递归后的执行次数,则时间复杂度为O(N)

例7: 计算斐波那契递归fibonacci的时间复杂度?
int fibonacci(int N) {
        return N < 2 ? N : fibonacci(N-1)+fibonacci(N-2);
    }
7 通过计算分析发现基本操作递归了2^n次,时间复杂度为O( 2^N)

 2.3空间复杂度

2.3.1概念

空间复杂度是对一个算法在运行过程中 临时占用存储空间大小的量度
空间复杂度算的是变量的个数。空间复杂度计算规则基本跟时间复杂度类似,也使用 O 渐进表示法

2.3.2常见空间复杂度计算举例

例1:计算bubbleSort的空间复杂度?

void bubbleSort(int[] array) {
        for (int end = array.length; end > 0; end--) {
            boolean sorted = true;
            for (int i = 1; i < end; i++) {
                if (array[i - 1] > array[i]) {
                    Swap(array, i - 1, i);
                    sorted = false;
                }
            }
            if (sorted == true) {
                break;
            }
        }
    }

1使用了常数个额外空间,所以空间复杂度为O(1)

例2:计算fibonacci的空间复杂度?

int[] fibonacci(int n) {
        long[] fibArray = new long[n + 1];
        fibArray[0] = 0;
        fibArray[1] = 1;
        for (int i = 2; i <= n; i++) {
            fibArray[i] = fibArray[i - 1] + fibArray[i - 2];
        }
        return fibArray;
    }
2 动态开辟了 N 个空间,空间复杂度为O(N)
例3:计算阶乘递归Factorial的空间复杂度?
long factorial(int N) {
        return N < 2 ? N : factorial(N - 1) * N;
    }
3 递归调用了 N 次,开辟了 N 个栈帧,每个栈帧使用了常数个空间。空间复杂度为 O(N)

3.包装类

Java 中,由于基本类型不是继承自 Object ,为了在泛型代码中可以支持基本类型, Java给每个基本类型都对应了一个包装类型。

3.1基本数据类型和对应的包装类

注:除了 Integer Character, 其余基本类型的包装类都是首字母大写。

3.2装箱和拆箱

    int i = 10;
    // 装箱操作,新建一个 Integer 类型对象,将 i 的值放入对象的某个属性中
    Integer ii = Integer.valueOf(i);
    Integer ij = new Integer(i);
    // 拆箱操作,将 Integer 对象中的值取出,放到一个基本数据类型中
    int j = ii.intValue();

4.泛型

4.1概念

泛型是 适用于许多许多类型 。从代码上讲,就是对类型实现了参数化。

4.2引出泛型

实现一个类,类中包含一个数组成员,使得数组中可以存放任何类型的数据,也可以根据 成员方法 返回数组中某个下标的值。
class MyArray {
    public Object[] array = new Object[10];
    public Object getPos(int pos) {
        return this.array[pos];
    }
    public void setVal(int pos,Object val) {
        this.array[pos] = val;
    }    
 }
 public class TestDemo {
    public static void main(String[] args) {
        MyArray myArray = new MyArray();
        myArray.setVal(0,10);
        myArray.setVal(1,"hello");//字符串也可以存放
        String ret = myArray.getPos(1);//编译报错
        //强行转化
        String ret =(String)myArray.getPos(1);
        System.out.println(ret);
    }
 }

通过运行上面的代码我们可以发现以下的问题:

1. 任何类型数据都可以存放。
2. 1 号下标本身就是字符串,但是确编译报错。必须进行强制类型转换。
因此在更多情况下,我们希望它只能够持有一种数据类型。而不是同时持有这么多类型。 所以泛型的主要目的:就是指定当前的容器,要持有什么类型的对象。让编译 器去做检查。此时,就需要把类型,作为参数传递。需要什么类型,就传入什么类型。

4.3语法

class MyArray<T> {//添加< >表示这个类是泛型
    public T[] array = (T[])new Object[10];//这句代码是错误的,这样写只是为了不让编译器报错
    public T getPos(int pos) {
        return this.array[pos];
    }
    public void setVal(int pos,T val) {
        this.array[pos] = val;
    }
 }
 public class TestDemo {
    public static void main(String[] args) {
        MyArray<Integer> myArray = new MyArray<>();
        //传入<Integer>后,每次存储数据时会检查存入的数据是不是我传入的类型,获取数据的时候也不需要强制转化。
        myArray.setVal(0,10);
        myArray.setVal(1,12);
        int ret = myArray.getPos(1);
        System.out.println(ret);
        myArray.setVal(2,"bit");
    }
 }
对上面代码进行解释:
1. 类名后的 <T> 代表占位符,表示当前类是一个泛型类。
了解: 【规范】类型形参一般使用一个大写字母表示,常用的名称有:

  • E 表示 Element
  • K 表示 Key
  • V 表示 Value
  • N 表示 Number
  • T 表示 Type
  • S, U, V 等等 - 第二、第三、第四个类型

2. 注释1处,不能new泛型类型的数组

 意味着:T[] ts = new T[5];//是不对的 

3. 注释 2 处,类型后加入 <Integer> 指定当前类型
4. 注释 3 处,不需要进行强制类型转换
5. 注释 4 处,代码编译报错,此时因为在注释 2 处指定类当前的类型,此时在注释 4 处,编译器会在存放元素的时候帮助我们进行类型检查。

5.泛型类的使用

5.1语法

泛型类<类型实参> 变量名; // 定义一个泛型类引用
new 泛型类<类型实参>(构造方法实参); // 实例化一个泛型类对象

举个例子:

MyArray<Integer> list = new MyArray<Integer>();
注意:泛型只能接受类,所有的基本数据类型必须使用包装类!

5.2类型推导

类型推导即当编译器可以根据上下文推导出类型实参时,可以省略类型实参的填写。
MyArray<Integer> list = new MyArray<>(); // 可以推导出实例化需要的类型实参为 Integer

 6.泛型的上界

6.1概念

泛型的上界:在定义泛型类时,有时需要对传入的类型变量做一定的约束,可以通过类型边界来约束。

6.2语法

class 泛型类名称<类型形参 extends 类型边界> {
...
}

举个例子:

public class MyArray<E extends Number> {
...
}

MyArray<Integer> l1; // 正常,因为 Integer 是 Number 的子类型
MyArray<String> l2; // 编译错误,因为 String 不是 Number 的子类型

tips: 没有指定类型边界 E,可以视为 E extends Object

7.泛型方法

泛型方法定义语法:

方法限定符 <类型形参列表> 返回值类型 方法名称(形参列表) { ... }

下面举个例子给大家看看:

public class Util {
    //静态的泛型方法 需要在static后用<>声明泛型类型参数
    public static <E> void swap(E[] array, int i, int j) {
        E t = array[i];
        array[i] = array[j];
        array[j] = t;
    }
}

8.总结

以上内容让大家能够初步认识数据结构的一些基本知识,后面还将继续与大家分享数据结构中的顺序表、链表、栈、队列、二叉树等内容,小欣建议大家在学数据结构的时候多画图、多动手与思考,才能更好地学习数据结构!

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值