Android数据结构与算法(一):基础简介

版权声明:本文出自汪磊的博客,未经作者允许禁止转载。

一、前言

项目进入收尾阶段,忙忙碌碌将近一个多月吧,还好,不算太难,就是麻烦点。

数据结构与算法这个系列早就想写了,一是梳理总结,顺便逼迫自己把一些模模糊糊的概念弄明白,最重要的我觉得数据结构与算法平时我们总是接触,什么ArrayList,LinkedList,HashMap...这些我们总是接触,但是在使用的时候有多少是会考虑一下性能方面问题,是不是你也不管三七二十一上来就是ArrayList,往里面扔数据呗,反正运行一般没问题,就像写文章一逗到底,一个字:爽。

此外,对于个人长远发展也十分重要的,基础扎实,不惧任何所谓的新技术。好了,本篇比较简单,介绍一下时间与空间复杂度概念,以及基本数据结构:数组,链表以及哈希表。

二、时间复杂度与空间复杂度概念

时间复杂度

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

这样用大写O()来体现算法时间复杂度的记法,我们称之为大0记法

以上是官方说法,我懂你现在的心情,这尼玛什么玩意,我用人话翻译一下哈:在分析算法时间复杂度时我们要找到一个函数f(n),使得当n无限大时能近似表示函数语句的执行次数,此时函数f(n)就可以近似表示此算法的时间复杂度。

咦?时间哪去了?怎么来了个算法的执行次数?其实真正的算法执行时间怎么可能用函数表示出来,只有跑起来才能知道算法执行时间,但是算法的执行次数是可以知道的,然而至于算法执行时间无非就是执行次数*每次执行时间,每次执行时间近似差不多的,所以也就可以用执行次数来大体估算算法的执行时间了。那怎么计算呢?继续看。

计算时间复杂度

首先算出算法执行次数的函数f(n),然后进行下面操作:

1、用常数1取代运行时间中的所有加法常数。

2、在修改后的运行次数函数中,只保留最高阶项。

3、如果最高阶项存在且不是1,则去除与这个项目相乘的常数。得到的结果就是大O阶。

我知道上面三条你又觉得蛋疼了,我们以实际例子为例讲解一下:

时间复杂度计算例子:

1 int sum = 0, n = 100;
2 sum = (1 + n) * n / 2;
3 printf("%d",sum);

三行代码总共执行了三次,所以此算法执行次数函数:

f(n) = 3.执行上面操作:

1、用常数1取代运行时间中的所有加法常数,f(n) = 3函数很简单,就一个常数3,被1取代,所以变为:

f(n) = 1

2、在修改后的运行次数函数中,只保留最高阶项。

3、如果最高阶项存在且不是1,则去除与这个项目相乘的常数。得到的结果就是大O阶。

至于2,3条此函数根本就没有最高阶项,所以也就无从谈起了,所以最终时间复杂度函数表示为O(1).

上面例子很简单,我们再看一个复杂点的:

1 int i, j;      
2 
3 for(i = 0; i < n; i++){
4 
5     for(j = i; j < n; j++){ //j = i,不是0  
6        
7     }
8 }

先算一下算法执行次数:

当i = 0时,内层循环执行n次,当i = 1时,内层循环执行n-1次,当i = 2时,内层循环执行n-2次,当i = n-1时,内层循环执行1次,当i = n时,内层循环执行0次,所以此算法执行函数为:

                f(n) = 0 +1+2+3+...+(n-1)+n = n(n+1)/2 = n2/2 + n/2

接下来执行上面操作,取近似表示法:

1、用常数1取代运行时间中的所有加法常数

次函数没有常数项所以此条不用操作了

2、在修改后的运行次数函数中,只保留最高阶项

次函数最高阶项为n2/2,所以简化后函数变为f(n) = n2/2

3、如果最高阶项存在且不是1,则去除与这个项目相乘的常数。得到的结果就是大O阶

最高阶项系数为1/2,不为1,去除,所以最终f(n) = n2

最终此算法时间复杂度表示为O(n2)

好了,关于时间复杂度想说的就这些吧,关键理解时间复杂度表示的意义以及一些常见算法时间复杂度的计算。

空间复杂度

此部分参考:https://blog.csdn.net/daijin888888/article/details/66970902

我们在写代码时,完全可以用空间来换取时间,比如说,要判断某某年是不是闰年,你可能会花一点心思写了一个算法,而且由于是一个算法,也就意味着,每次给一个年份,都是要通过计算得到是否是闰年的结果。 还有另一个办法就是,事先建立一个有2050个元素的数组(年数略比现实多一点),然后把所有的年份按下标的数字对应,如果是闰年,此数组项的值就是1,如果不是值为0。这样,所谓的判断某一年是否是闰年,就变成了查找这个数组的某一项的值是多少的问题。此时,我们的运算是最小化了,但是硬盘上或者内存中需要存储这2050个0和1。这是通过一笔空间上的开销来换取计算时间的小技巧。到底哪一个好,其实要看你用在什么地方。
    算法的空间复杂度通过计算算法所需的存储空间实现,算法空间复杂度的计算公式记作:S(n)= O(f(n)),其中,n为问题的规模,f(n)为语句关于n所占存储空间的函数。
    一般情况下,一个程序在机器上执行时,除了需要存储程序本身的指令、常数、变量和输入数据外,还需要存储对数据操作的存储单元,若输入数据所占空间只取决于问题本身,和算法无关,这样只需要分析该算法在实现时所需的辅助单元即可。若算法执行时所需的辅助空间相对于输入数据量而言是个常数,则称此算法为原地工作,空间复杂度为0(1)。
     通常, 我们都使用"时间复杂度"来指运行时间的需求,使用"空间复杂度"指空间需求。当不用限定词地使用"复杂度'时,通常都是指时间复杂度。

最后附上常见排序算法时间复杂度与空间复杂度:

好了,关于时间与空间复杂度就聊到这里。

三、数据结构之数组

 数组你我很熟悉了,这里只是总结一下。

数组内存中存储方式如下:

 

数组在内存中是一块连续的存储单元存储起来的,声明数组的时候我们必须声明其长度,这样才会为我们声明一个连续的存储区域。

这种存储方式造成我们想要往数组中存储一个数据时那么其后面各个元素都要往后移动,同样的,删除数据后面的数据都要往前移动。

但是同样也带来好处,我们要想获取数组中第i个元素,直接通过角标获取即可,同理修改也是。

数组获取第i个数据的算法时间复杂度为O(1),直接通过角标获取即可,执行一次。

数组查找其内是否包含某一元素表现不好,因为只有挨个比较才可以,试想一下数组特别大的情况下,挨个比较一下还是挺费劲的,此算法时间复杂度为O(n)。

简单总结:数组获取某一数据很简单通过角标i直接获取即可,但是增删比较低效,在内存中用一块连续的存储区域来存储,查找数组中是否包含某一元素比较低效。

四、数据结构之链表

与数组不同,链表不用非要一块连续的存储区域,链表是一种离散存储结构,数据之间通过指针链接,每个数据元素包含数据域与指针域,数据域存储对应数据即可,而指针域则指向下一个数据元素(对于单项链表来说),针对指针域还可以分为单向链表,双向链表,循环链表。

单项链表存储模型:

对于双向链表就是一个数据元素包含三部分:数据域,前向指针,后向指针。前向指针指向前一个数据元素,后向指针就像单向链表一样指向后一个数据元素:

循环链表就是在双向链表基础上将首尾数据元素通过指针连接起来:

 三种链表大体如上,顺带秀了一下我强大的画图能力,哈哈...

由于链表的离散存储方式,使其与数组有很大区别;

链表要是想获取某一个元素那可费劲了,需要我们从第一个元素开始挨个遍历查找,修改也同理,因为当前元素地址存储在上一个元素的后向指针里,所以只能从头挨个遍历查找,所以相对于数组链表存储方式获取元素,修改元素效率低。

链表对于插入,删除一个数据元素效率比较高,不像数组那样影响后续所以元素,只需操作前后元素的指针即可。

链表不要求必须有一块连续的存储区域,所以不会造成内存碎片化。

链表中对于查找某一元素是否在链表中与数组一样也需要挨个比较每个元素,所以也比较低效,时间复杂度为O(n)。

与数组相比每一个数据项占用内存会变多,因为要多存储指针域。

简单总结:链表增删效率高,查找效率低。每一个数据项与数组相比更耗内存。不需要整块内存块,不会造成碎片化。

五、数据结构之哈希表

上面分析可以看到无论数组还是链表对于查找一个数据是否存在数组中或者链表中效率都比较低,都需要挨个比较,那有没有一种办法可以消除一部分数据的对比呢?先人们想来想去,哈希表横空出世了。

哈希表就是一种以键-值(key-indexed) 存储数据的结构,我们只要输入待查找的值即key,即可查找到其对应的值。

存储时key类型是不确定的,可能是int,可能是String,也可能是其他任意对象。Hash函数的作用就是把这些对象通过合理的方式转为int类型,从而完成数据的存储。

更形象类比就是字典了(几乎所有讲到哈希表的博文都用此类比),小时候我们查字典会先根据拼音去查对应页数,然后再去对应页数去查询要找的字,哈希表也是同样道理,此处不过多表述。

哈希碰撞问题

提到哈希表,必然提一下哈希碰撞问题,哈希碰撞两个不同的原始值在经过哈希运算后得到同样的结果,这样就是哈希碰撞。这里就有疑问了?为什么两个不同的值经过哈希运算后还会得到相同的值呢?简单来说哈希算法并不完美,是会出现这种情况的,此处就不必深究了,如果你时间充足,那就去深入研究一下哈希算法吧。

哈希碰撞解决办法有开放定址法,链地址法等等,我们需要额外关注链地址法。

链地址法其实就是HashMap中用的策略,数组+链表的实现形式。

原理是在HashMap中同样哈希值的位置以一串链表存储起来数据,把多个原始值不同而哈希结果相同的数据以链表存储起来,类似如下:

 

 哈希表优缺点:

哈希表能同时兼备数组和链表的优点,它能在插入和查找时都具备良好的性能。
设计不好的哈希表,有可能会出现较多的哈希碰撞,导致链表过长,从而哈希表会更像一个链表。
还有当数据量很大时,为防止单个链表过长,就需要对数组进行扩容,对性能的影响也很严重。
 
好了,本篇到这里就该结束了,整篇没有什么源码方面的分析,放心后续会有大量源码解析的,先把一些基础概念搞清楚吧,希望对你有用。
 
声明:文章将会陆续搬迁到个人公众号,以后文章也会第一时间发布到个人公众号,及时获取文章内容请关注公众号
 
posted @ 2018-07-18 10:50 WangLei_ClearHeart 阅读(...) 评论(...) 编辑 收藏