Java数据结构

Java数据结构

1、数据结构概述

1、什么是数据结构

官方解释:数据结构是- -i门研究非数值计算的程序设计问题中的操作对象,以及他们之间的关系和操作等相关问题
的学科。
**说人话:**数据结构就是把数据元素按照一定的关系组织起来的集合, 用来组织和存储数据。

2、数据结构的分类

逻辑结构

逻辑结构是从具体问题中抽象出来的模型,是抽象意义上的结构,按照对象中数据元素之间的相互关系分类,也是
我们后面课题中需要关注和讨论的问题。

**集合结构:**集合结构中数据元素除了属于同一个集合外,它们之间并无任何关系

image-20210513191037910

**线性结构:**线性结构中的数据元素之间存在一对一的关系。

image-20210513191112528

**树形结构:**树形结构中的数据元素之间存在一对多的层次关系

image-20210513191151636

**图形结构:**图形结构的元素数据是多对多的关系

image-20210513191226057

物理结构

逻辑结构在计算机中真正的表示方式(又称为映像)称为物理结构,也可以叫做存储结构。常见的物理结构有顺序
存储结构、链式存储结构。

**顺序结构:**把数据元素放到地址连续的存储单元里面,其数据间的逻辑关系和物理关系是一致的, 比如数组。

image-20210513191344476

顺序存储结构存在一定的弊端, 就像生活中排时也会有人插队也可能有人有特殊情况突然离开,这时候整个结构都
处于变化中,此时就需要链式存储结构。

**链式结构:**是把数据元素存放在任意的存储单元里面,这组存储单元可以是连续的也可以是不连续的。此时,数据
元素之间并不能反映数据之间的逻辑关系,因此在链式存储结构中引进了一个指针存放数据元素的地址,这样通过
地址就可以找到相关的数据元素的位置。

image-20210513191611590

3、什么是算法

**官方解释:**算法是指解题方案的准确而完整的描述,是- -系列解决问题的清晰指令,算法代表着用系统的方法解决
问题的策略机制。也就是说,能够对一定规范的输入,在有限时间内获得所要求的输出。

**说人话:**根据一定的条件,对一些数据进行计算,得到需要的结果。

一个优秀的算法追求以下两个目标:

  1. 花最少的时间完成需求;
  2. 占用最少的内存空间完成需求;

2、算法时间复杂度分析

我们要计算算法时间耗费情况,首先我们得度量算法的执行时间,那么如何度量呢?

事后分析估算方法:

​ 比较容易想到的方法就是我们把算法执行若干次,然后拿个计时器在旁边计时,这种事后统计的方法看上去的确不
错,并且也并非要我们真的拿个计算器在旁边计算,因为计算机都提供了计时的功能。这种统计方法主要是通过设
计好的测试程序和测试数据,利用计算机计时器对不同的算法编制的程序的运行时间进行比较,从而确定算法效率
的高低,但是这种方法有很大的缺陷:必须依据算法实现编制好的测试程序,通常要花费大量时间和精力,测试完
了如果发现测试的是非常糟糕的算法,那么之前所做的事情就全部白费了,并且不同的测试环境(硬件环境)的差别
导致测试的结果差异也很大。

事前分析估算方法:
在计算机程序编写前,依据统计方法对算法进行估算,经过总结,我们发现一个高级语言编写的程序程序在计算机
上运行所消耗的时间取决于下列因素:

  1. 算法采用的策略和方案;
  2. 编译产生的代码质量;
  3. 问题的输入规模(所谓的问题输入规模就是输入量的多少;
  4. 机器执行指令的速度;

​ 由此可见,抛开这些与计算机硬件、软件有关的因素,一个程序的运行时间依赖于算法的好坏和问题的输入规模。
如果算法固定,那么该算法的执行时间就只和问题的输入规模有关系了。

在研究算法的效率时,我们只考虑核心代码的执行次数,这样可以简化分析。

我们研究算法复杂度,侧重的是当输入规模不断增大时,算法的增长量的一个抽象(规律),而不是精确地定位需要
执行多少次,因为如果是这样的话,我们又得考虑回编译期优化等问题,容易主次跌倒。

1、大O记法

定义: 在进行算法分析时,语句总的执行次数T(n)是关于问题规模n的函数,进而分析T(n)随着n的变化情况并确定
T(n)的量级。算法的时间复杂度,就是算法的时间量度,记作:T((n)=O(n))。 它表示随着问题规模n的增大,算法执
行时间的增长率和f(n)的增长率相同,称作算法的渐近时间复杂度,简称时间复杂度,其中f(n)是问题规模n的某个
函数。

在这里,我们需要明确一个事情: 执行次数=执行时间

用大写0()来体现算法时间复杂度的记法,我们称之为大0记法。-般情况下,随着输入规模n的增大,T(n)增长最
慢的算法为最优算法。

如果用大0记法表示上述每个算法的时间复杂度,应该如何表示呢?基于我们对函数渐近增长的分析,推导大0阶
的表示法有以下几个规则可以使用:

  1. 用常数=1=取代运行时间中的所有加法常数
  2. 在修改后的运行次数中,只保留高阶项;
  3. 如果最高阶项存在,且常数因子不为1,则去除与这个项相乘的常数;

2、常见的大O阶

1.线性阶

一般含有非嵌套循环涉及线性阶,线性阶就是随着输入规模的扩大,对应计算次数呈直线增长,例如:

public class study {
    public static void main(String[] args) {
        long start = System.nanoTime();
        int sum = 0;
        int count = 100;
        for (int i = 0; i <= count; i++) {
            sum+= i ;
        }
        System.out.println(sum);
        System.out.println("耗时" + (System.nanoTime()-start));
    }
}
2.平方阶

一般嵌套循环属于这种时间复杂度

public class study {
    public static void main(String[] args) {
        long start = System.nanoTime();
        int sum = 0;
        int n = 100;
        for (int i = 0; i <= n; i++) {
            for (int j = 0; j <= n ; j++) {
                sum += n;
            }
        }
        System.out.println(sum);
        System.out.println("耗时" + (System.nanoTime()-start));
    }
}

上面这段代码,n=100, 也就是说,外层循环每执行一-次, 内层循环就执行100次,那总共程序想要从这两个循环
中出来,就需要执行100*100次,也就是n的平方次,所以这段代码的时间复杂度是0(n^2).

3.立方阶

一般三层嵌套循环属于这种时间复杂度

public class study {
    public static void main(String[] args) {
        long start = System.nanoTime();
        int sum = 0;
        int n = 100;
        for (int i = 0; i <= n; i++) {
            for (int j = 0; j <= n ; j++) {
                for (int k = 0; k <= n ; k++) {
                    sum += n;
                }
            }
        }
        System.out.println(sum);
        System.out.println("耗时" + (System.nanoTime()-start));
    }
}

上面这段代码,n=100, 也就是说,外层循环每执行一次,中间循环循环就执行100次,中间循环每执行一次,最
内层循环需要执行100次,那总共程序想要从这三个循环中出来,就需要执行100100100次,也就是n的立方,
所以这段代码的时间复杂度是O(n^3)

4.对数阶
public class study {
    public static void main(String[] args) {
        int i = 1;
        int n = 100;
        while (i<n){
            //每次循环都扩大2倍
            i = i*2;
        }
        System.out.println(i);
    }
}

由于每次i*2之后,就距离n更近-步,假设有x个2相乘后大于n,则会退出循环。由于是2^x=n,得到x=log(2)n,所
以这个循环的时间复杂度为(logn);
对于对数阶,由于随着输入规模n的增大,不管底数为多少,他们的增长趋势是一样的,所以我们会忽略底数

5.常数阶

一般不涉及循环操作的都是常数阶,因为它不会随着n的增长而增加操作次数。例如:

public class study {
    public static void main(String[] args) {
        int sum = 1;
        int n = 10000;
        sum = (n+1) * n/2;
        System.out.println(sum);
    }
}

image-20210513200729642

他们的复杂程度从低到高依次为:
0(1)<0logn)<O(n)<O(nlogn)<O(n2)<O(n3)<O(2^N)

我们的算法,尽可能的追求的是0(1),O(logn),O(n),O(nlogn)这几种时间复杂度, 而如果发现算法的时间复杂度为平
方阶、立方阶或者更复杂的,那我们可以分为这种算法是不可取的,需要优化。

3、最坏情况

算法分析也是类似,假如有一个需求:
有一个存储了n个随机数字的数组,请从中查找出指定的数字。

public class study {
    public static void main(String[] args) {
        int sum = 1;
        int n = 1;
        int[] arr = {11,3,112,45,6,878,3,1};
        for (int i = 0; i < arr.length; i++) {
            if (n==arr[i]){
                System.out.println(i);
                break;
            }
        }
    }
}

最好情况:
查找的第-个数字就是期望的数字,那么算法的时间复杂度为O(1)

最坏情况:
查找的最后一个数字,才是期望的数字,那么算法的时间复杂度为O(n)

平均情况:
任何数字查找的平均成本是O(n/2)

最坏情况是一种保证, 在应用中,这是-种最基本的保障,即使在最坏情况下,也能够正常提供服务,所以,除非
特别指定,我们提到的运行时间都指的是最坏情况下的运行时间。

3、算法空间复杂度分析

计算机的软硬件都经历了一个比较漫长的演变史,作为为运算提供环境的内存,更是如此,从早些时候的512k,经
历了1M,2M. 4M…等, 发展到现在的8G,甚至16G和32G,所以早期,算法在运行过程中对内存的占用情况也
是一个经常需要考虑的问题。我么可以用算法的空间复杂度来描述算法对内存的占用。

1、 Java中常见的内存占用

image-20210518140207650

2.计算机访问内存的方式都是一次一个字节

3.一个引用需要8个字节表示
例如: Date date= new Date(),则date这个变量需要占用8个字节来表示

4.创建一个对象,比如 new Date( ),除了Date对象内部存储的数据例如年月日等信息)占用的内存,该对象本身
也有内存开销,每个对象的自身开销是16个字节,用来保存对象的头信息

5.一般内存的使用,如果不够8个字节,都会自动填充为8字节。

public class tset {
    private int age = 1;

    public static void main(String[] args) {
        new tset();
    }
}
  • new Person( )对象占16字节
  • int age 占4个字节,但是不够8字节,会自动填充为8字节
  • 所以 new Person( ) 的过程占了24字节

6.Java中数组被被限定为对象,他们一般都会因为记录长度而需要额外的内存,一个原始数据类型的数组一般需
要24字节的头信息(16个自己的对象开销,4字节用于保存长度以及4个填充字节)再加上保存值所需的内存。

由于Java中有内存垃圾回收机制,并m对程序的内存占用也有优化(例妙如即时编译),我们无法精确的评估一
个Java程序的内存占用情兄,但是了解了Java的基本内存占用,使我们可以对Java程序的内存占用情况进行估算。
由于现在的计算机设备内存一般都比较大,基本上个人计算机都是4G起步,大的可以达到32G,所以内存占用一
般情况下并不是我们算法的瓶颈,普通情况下直接说复杂度,默认为算法的时间复杂度
但是,如果你做的程序是嵌入式开发,尤其是一些传感器设备上的内置程序,由于这些设备的内存很小,一般为几
kb,这个时候对算法的空间复杂度就有要求了,但是一般做Java开发的,基本上都是服务器开发,一般不存在这
样的问题。

4、排序算法

1. 冒泡排序

需求:
排序前: {4,5,6,3,2,1}
排序后: {1,2,3,4,5,6}

排序原理:

  1. 比较相邻的元素。如果前一个元素比后一个元素大,就交换这两个元素的位置。
  2. 对每- -对相邻元素做同样的工作,从开始第一对元素到结尾的最后一对元素。最终最后位置的元素就是最大
    值。

image-20210518152709580

package sort;

import java.util.Arrays;

// 冒泡排序
public class Bubble {

    public static void sort(int[] arr) {
        //外层循环的目的是为了让元素从末尾开始冒泡
        for (int i = arr.length - 1; i > 0; i--) {
            //遍历实际的数组
            for (int j = 0; j < i; j++) {
                //判断当前元素是否比下一个大
                if (greater(arr[j], arr[j + 1])) {
                    //如果大则交换位置
                    exch(arr, j, j + 1);
                }
            }
        }
    }

    private static boolean greater(int v, int w) {
        return v > w;
    }

    //交换数组下标
    private static void exch(int[] a, int i, int j) {
        int temp = a[i];
        a[i] = a[j];
        a[j] = temp;
    }

}

class Test {
    public static void main(String[] args) {
        int[] arr = {4, 5, 6, 3, 2, 1};
        Bubble.sort(arr);
        System.out.println(Arrays.toString(arr));
    }
}

冒泡排序的时间复杂度分析冒泡排序使用了双层for循环,其中内层循环的循环体是真正完成排序的代码,所以,
我们分析冒泡排序的时间复杂度,主要分析一下内层循环体的执行次数即可。
在最坏情况下,也就是假如要排序的元素为{6,5,4,3,2,1}逆序,那么:
元素比较的次数为:
(N-1)+(N-2)+(33+…+2+1=((N-1)+1)(N-1)/2=N^2/2-N/2;
元素交换的次数为:
(N-1)+(N-2)+(-3)…+2+ 1=((N-1)+1)
(N-1)/2=N^2/2-N/2;
总执行次数为:
(N2/2-N/2)+(N2/2-N/2)=N^2-N;
按照大O推导法则,保留函数中的最高阶项那么最终冒泡排序的时间复杂度为O(N^2).

2. 选择排序

选择排序是一种更加简单直观的排序方法
需求:
排序前:{4.6,8,7,9,2,1}
排序后:{1,2,4,5,7,8,9
排序原理:

  1. 每一次遍历的过程中,都假定第一个索引处的元素是最小值,和其他索引处的值依次进行比较,如果当前索引
    处的值大于其他某个索引处的值,则假定其他某个索引出的值为最小值,最后可以找到最小值所在的索引
  2. 交换第一个索引处和最小值所在的索引处的值

image-20210518160040453

public class Selection {
    public static void sort(int[] arr) {
        for (int i = 0; i <= arr.length - 2; i++) {
            //假设本次遍历,第i个位置是最小值
            int minIndex = i;
            for (int j = i + 1; j < arr.length; j++) {
                if(greater(arr[minIndex],arr[j])){
                    //更换最小值
                    minIndex = j;
                }
            }
            //比较完了,当前的minIndex就是本次循环的最小值下标
            exch(arr,i,minIndex);
        }
    }

    private static boolean greater(int v, int w) {
        return v > w;
    }

    //交换数组下标
    private static void exch(int[] a, int i, int j) {
        int temp = a[i];
        a[i] = a[j];
        a[j] = temp;
    }
}

class Test {
    public static void main(String[] args) {
        int[] a = {4, 5, 8, 7, 0, 2, 1};
        Selection.sort(a);
        System.out.println(Arrays.toString(a));
    }
}

选择排序的时间复杂度分析:
选择排序使用了双层for循环,其中外层循环完成了数据交换,内层循环完成了数据比较,所以我们分别统计数据
交换次数和数据比较次数:
数据比较次数
(N-1)+(N-2)+(N-3)+.+2+1=(N-1)+1) * (N-1)/2=N2/2-N/2
数据交换次数
时间复杂度:N^2/2-N2+(N-1) = N^2/2+N/2-1
根据大0推导法则,保留最高阶项,去除常数因子,时间复杂度为O(N^2)

3.插入排序

排序前: {4,3,2,10,12,1,,5,6}
排序后: {1,2,3,4,5,6,10,12}
排序原理:
1.把所有的元素分为两组,已经排序的和未排序的
2找到未排序的组中的第一个元素,向已经排序的组中进行插入
3倒叙遍历已经排序的元素,依次和待插入的元素进行比较。直到找到一个元素小于等于待插入元素,那么就把待
插入元素放到这个位置,其他的元素向后移动位

image-20210520154313639

public class Insertion {
    public static void sort(int[] arr) {
        for (int i = 1; i < arr.length; i++) {  //遍历所有数组
            for (int j = i; j > 0; j--) {       //选择那个遍历的数组与前面的值进行比较
                if (greater(arr[j-1],arr[j])){
                    exch(arr,j-1,j);          //如果值比前面小就进行交换
                }else {
                    break;
                }
            }
        }
    }

    private static boolean greater(int v, int w) {
        return v > w;
    }

    //交换数组下标
    private static void exch(int[] a, int i, int j) {
        int temp = a[i];
        a[i] = a[j];
        a[j] = temp;
    }
}

class Test {
    public static void main(String[] args) {
        int[] arr={4,3,2,10,12,1,5,6};
        Insertion.sort(arr);
        System.out.println(Arrays.toString(arr));
    }
}
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值