数据结构与算法

数据结构与算法
1.数据结构的概念
数据结构指的是一组数据的存储结构。
2.算法的概念
算法是指操作数据的一组方法
3.二者的关系
数据结构是为算法服务的,而算法要作用在特定的数据结构上。
4.最常用的数据结构预算法

  • 数据结构:数组、链表、栈、队列、散列表、二叉树、堆、跳表、图、Tire树

  • 算法:递归、排序、二分查找、搜索、哈希算法、贪心算法、分治算法、回溯算法、动态规划、字符串匹配算法。
    一、算法的复杂度
    1.1大O复杂度表示法:
    T(n)=O(f(n)).
    其中T(n)表示代码执行的时间;n表示数据规模的大小;f(n)表示每段代码执行的次数总和。所以用f(n)来表示。公式中的O,表示代码的执行时间T(n)与f(n)表达式成正比。
    大O复杂度算法:可以类似于数学中可以省略一些没有必要的相关的参数,只保留了那些只与重要的项的参数而已,大O时间复杂度实际上并不是代码真正的执行时间,而是代码执行时间随数据规模增长的变化趋势,也叫做渐进时间复杂度,简称时间复杂度。
    在大O复杂度计算的方法中,如果当n很大时,可以将其想象成1000或者10000,而且公式中的低阶、常量、系数三部分并不左右增长趋势,其在一定的公式计算中可以忽略,我们只需要记录一个最大量级就可以了,也就是找到其中关于在公式中最大量级单位。
    1.2复杂度分析法则
    (1).单段代码看高频:比如循环。
    (2).多段代码取最大:比如一段代码中有单循环和多重循环,那么在这种情况下取多重循环的复杂度。
    (3).如果是在嵌套代码中则求其乘积:比如在递归中和在多重循环中。
    (4).多个代码求加法,如果是在多个代码的情况下,比如方法有两个参数控制两个循环的次数,那么这时就取二者复杂度的相加
    1.3时间复杂度的分析

  • 只关注执行次数最多的一段代码

  • 加法法则:总复杂度等于量级最大的那段代码的复杂度。

  • 乘法法则:嵌套代码的复杂度等于嵌套内外代码复杂度的乘积。
    1.4几种常见时间复杂度实例分析
    复杂度量级(按照数量级递增)

  • 常量阶O(1)

  • 对数阶O(logn)

  • 线性阶O(n)

  • 线性对数阶O(nlogn)

  • 平方阶O(n^2)

  • 指数阶O(2^n)

  • 阶乘阶O(n!)
    多项式阶:随着数据规模的增长,算法的执行时间和空间占用,按照多项式的比例增长。包括常数阶、对数阶、线性阶、线性对数阶、平方阶、立方阶。
    非多项式阶:随着数据规模的增长,算法的执行时间和空间占用暴增,这类算法的性能级差,包括指数阶、阶乘阶。
    常量级时间复杂度,只要代码的执行时间不随n的增大而增长,这样代码的时间复杂度我们都记作O(1)。
    再例如:

      while(i<=n)
      {
      i=i*2;
      }

2^0 2^1 2^2 2^3 2^4 2^5 2^6 …2^x =n
从这里面我们可以求得x=log2n,所以我们这段代码的 复杂度就是O(log2n)。
加法法则与乘法法则

int cal(int m, int n)//此处是定义两个数据规模m、n
{
int sum_1=0;//此处是先定义其初始变量之后再扩充
int i=1;
for( ;i<m;++i)
{
sum_1=sum_1+i;
}
int sum_2=0;//先定义其数据规模为0再利用变量j对其扩充
int j=1;
for( ;j<n;++j)
{
sum_2=sum_2+j;
}
return sum_1+sum_2;
}

从这段代码中可以看出,m,n是表示两个数据规模。我们无法事先估计m和n谁的量级大,所以我们在表示复杂度的时候,就不能简单地利用加法法则,省略掉其中一个。所以,上面代码的时间复杂度就是O(m+n)。针对这种情况,原来的加法法则就不正确了,我们需要将加法法则改为:T1(m)+T2(m
)=O(f(m)*g(n)),并且乘法法则则继续有效:T1(m)*T2(n)=O(f(m)*f(n)).
1.5空间复杂度分析
表示算法的存储空间与数据规模之间的增长关系

void print(int n)
{
int i=0;
int[a]=new int [n];
for(i;i<n;++i)
{
a[i]=i*i;
}
for(i=n-1;i>=0;--i)
{
print out a[i];
}
}

与时间复杂度分析一样,我们可以看到,在第三行代码中,我们申请了一个空间存储变量i,但是它是常量阶的,与数据规模n没有关系,所以我们可以忽略,在第四行申请了一个大小为n的int类型数组,除1此之外,剩下的代码都没有占用更多的存储空间,所以整段代码的空间复杂度就是O(n)。
我们常见的空间复杂度就是O(1)、O(n)、O(n2),就像O(logn)、O(nlogn)这样的对数阶复杂度平时都用不到。而且,空间复杂度分析比时间复杂度分析要简单很多。
1.6复杂度增长趋势图
在这里插入图片描述
分为最好情况时间复杂度、最坏时间复杂度、平均情况时间复杂度、均摊时间复杂度。
一、复杂度分析的四个概念
1.最坏情况时间复杂度:代码在最坏情况下执行的时间复杂度。
2.最好情况时间复杂度:代码在最理想情况下执行的时间复杂度。
3.平均时间复杂度:代码在所有情况下执行的次数的加权平均值。
4.均摊时间复杂度:在代码执行的所有复杂度情况中绝大部分是低级别的复杂度,个别情况是高级别复杂度且发生具有时序关系时,可以将高级别复杂度均摊到低级别复杂度上。基本上均摊结果就等于低级别复杂度。
二:为什么要引入这四个概念呢?
1.同一段代码在不同情况下时间复杂度会出现量级差异,为了更全面,更准确的描述代码的时间复杂度,所以引入这四个概念。
2.代码复杂度在不同情况下出现量级差别时才需要区别这四种复杂度。大多数情况下,是不需要区别分析它们的。
三:如何分析平均、均摊时间复杂度
1.平均时间复杂度
代码在不同情况下复杂度出现量级差别,则用代码所有可能情况下执行次数的加权平均值表示。
2.均摊时间复杂度
两个条件满足时使用:(1)、代码在绝大多数情况下是低级别复杂度,只有极少数情况是高级别复杂度。(2)、低级别和高级别复杂度出现具有时序规律。均摊结果一般都等于低级别复杂度。

1.数组

线性表: 线性表就是数据排成了一条像一条线的结构.每个线性表上的数据最多只有前后两个方向.常见的线性表结构“数组、链表、队列、栈等;
在这里插入图片描述
什么是数组:
1.数组是一种线性表数据结构。它用一组连续的内存空间,来存储一组具有相同类型的数据。
2.连续的内存空间和相同类型的数据(随机访问的前提条件)。
3.优点:两个限制使得具有其具有随机访问的特性缺点;删除和插入效率低。
数组是怎么根据下标随机访问的?
通过寻址公式:a[i]_address=base_address+i*data_type-size
其中data_type_size表示每个元素的大小,base_address是首元素地址,其中i是数组下标。
为何两数组插入和删除低效
插入:如果有一个元素想往int[n]的第k个位置插入数据,需要在k-n的位置往后移。
最好的情况时间复杂度O(1)。
在这里插入图片描述
如果数组中的数据并不是有序的,也就是无规律的情况下,可以直接把第k个位置上的数据移到最后,然后将插入的数据直接放到第k个位置上。
最坏的情况时间复杂度为O(n)在这里插入图片描述
平均复杂度为O(n)
2.低效的插入和删除
(1).插入:从最好O(1)最坏O(n)平均O(n)
(2).插入:插入数组若无序,插入新的元素时,可以将第k个位置元素移到数组末尾,把新的元素,插入到第k个位置,此处的复杂度为O(1)。
(3).删除:从最好O(1)最坏O(n)平均O(n)
(4).多次删除集中在一起,提高删除效率:
记录下已经被删除的数据,每次删除的操作并不是搬移数据,而是记录数据已经被删除,当数组没有更多的存储空间时,再触发一次真正的删除操作。即JVM标记清除垃圾回收算法。

2.链表

什么是链表
1.和数组一样,链表也是一种线性表。
2.从内存结构上来看,链表的内存结构是不连续的内存空间,是将一组零散的内存块串联起来,从而进行数据存储的数据结构。
3.链表的每一块内存块被称为Node。节点除了存储数据外,还需记录链上下一个节点的地址,即后继指针next。在这里插入图片描述
链表的特点
1.插入、删除数据效率高,是属于O(1)级别的,即只需要更改指针的指向即可,然而随机访问的效率低,是属于O(n)级别(需要从链头至链尾进行遍历)。

在这里插入图片描述
2.与数组相比,链表的内存消耗空间更大,因为每个存储数据结点都需要额外的空间存储后继指针即在每个数据结点之后呢多加一个指针用于指向下一个位置(称之为额外的空间存储后继指针)。

常用链表
1.单链表
在这里插入图片描述
如图所示这是一个单链表,其有如下特点:(1).每个结点只包含一个指针,即后继指针 (2).单链表有两个特殊的结点,即首结点和尾结点,为什么称之为特殊呢? 即用首结点地址表示整条链表,尾结点的后继指针指向空地址NULL。 (3).性能特点:插入和删除操作的时间复杂度为O(1),查找的时间复杂度为O(n) 总结:即在单链表中的特点是插入和删除的时间复杂度为O(1),查找的时间复杂度为O(n).

2.循环链表
在这里插入图片描述
(1).除了尾结点的后继指针指向首结点的地址外其余的均与单链表一致。
(2).适用于存储的循环特点的数据,比如约瑟夫问题。
3.双向链表
在这里插入图片描述
(1).节点除了存储数据外,还有两个指针分别指向前一个节点地址(即前驱指针prev)和下一个节点的位置(后继指针next)。
(2).首节点的前驱指针prev和尾结点的后继指针均指向空地址。
(3).性能特点:
与单链表相比,存储相同的数据,需要消耗更多的存储空间。
插入、删除操作比单链表的效率更高O(1)级别。以删除操作为例,删除操作分两种情况:1.给定数据值删除对应节点 2.给定节点地址删除节点。对于第一种情况而言单链表和双向链表都需要从头到尾进行遍历从而找到对应节点进行删除,时间复杂度为O(n),对于第二种情况而言:则要进行删除操作则必须要找到前驱节点,单链表需要从头到尾进行遍历直到P->next=q
在这里面呢。其时间复杂度为O(n),而双向链表可以找到前驱节点,时间复杂度为O(1),这就是在这种情况下双向链表优于单向链表的地方。
对于一个有序链表,双向链表的按值查询效率比单链表要高一些,因为我们可以记录上次查找的位置P,(有序链表中双向链表中可以记录上次查找的位置P),每一次查询时,根据要查找的值与P的大小关系,决定是往前还是往后查找(即根据这一点P的位置查找前一个或下一个要查找的目标的位置),所以平均而言只需要查找一半的数据就可以了。
4.双向循环链表
在这里插入图片描述
双向循环链表的特点呢?
为首节点的前驱指针指向尾结点,尾节点的后继指针指向首节点。
*一、选择数组还是链表?
1.数组: 插入、删除操作的时间复杂度为O(n),随机访问的时间复杂度为O(1).
2.链表: 插入、删除操作的时间复杂度为O(1),随机访问的时间复杂度为O(n).
*二、数组的缺点:
(1).如果申请的内存空间过大,比如100M,但是如果没有100M的连续内存空间,那么会申请失败,尽管内存的可用空间超过100M。
(2).大小固定,若存储空间不足,则需要扩容,一旦扩容就要进行数据复制,但是这是十分费时的。
*三、链表的缺点
(1).内存空间消耗更大,因为需要额外的空间存储指针信息.
(2).对于链表进行频繁的插入和删除操作,会导致频繁的内存申请和释放,容易造成内存碎片,然而如果是Java语言,还可能会造成频繁的GC(自动垃圾回收器)操作。
*四、如何选择
(1).数组简单易用,在实现上使用连续的内存空间,可以借助CPU的缓冲机制预读数组中的数据,所以访问效率更高。然而链表在内存中并不是连续存储的,所以对于CPU缓存不友好,没有办法预读。
(2).如果代码对内存的使用非常苛刻,那数组就更适合
*应用
1.如何分别用链表和数组实现LUR缓冲淘汰策略?
(1).什么是缓存?
缓存是一种提高数据读取性能的技术,在硬件设计、软件开发中都有着非广泛的应用,比如常见的CPU缓存、数据库缓存、浏览器缓存等等。
(2).为什么使用缓存?即缓存的特点?
缓存的大小是有限的,当缓存被用慢的时候,哪些数据应该被清理出去,哪些数据应该被保留?就需要用到缓存淘汰策略。
(3).什么是缓存淘汰策略?
缓存淘汰策略指的就是被用满时清理数据的优先顺序。
(4).有哪些缓存淘汰策略?
常见的3种包括先进先出策略FIFO(First In,First Out)、最少使用策略LFU(Least Frequently Used)、最近最少使用策略LRU(Least Recently Used).
(5).链表使用LRU缓存淘汰策略
当访问的数据没有存储在缓存的链表中时,直接将数据插入链表表头,时间复杂度为O(1);当访问的数据存在于存储的链表中时,将该数据对应的节点插入到链表表头,其时间复杂度为O(1)。
(6).数组实现LRU缓存淘汰策略
方式一:首位置保存最新访问数据,末位置优先处理
当访问的数据未存在于缓存的数组中时,直接将数据插入到数组第一个元素的位置,此时数组所有元素需要向后移动一个位置,时间复杂度为O(n);当访问的数据存在于缓存的数组中时,查找到数据并将其插入数组第一个位置,此时亦需移动数组元素,时间复杂度为O(n)。若缓存用满时,则清理掉末尾的数据,时间复杂度为O(1)。
方式二:首位置优先处理,末尾位置保存最新访问数据
当访问的数据未存在于缓存的数组中,直接将数据添加进数组作为当前只有一个元素,时间复杂度为O(1),当访问的数据存在于缓存的数组中,查找到数据并将其插入到当前数组 最后一个元素的位置,此时亦需要移动数组元素,时间复杂度为O(n)。当缓存用满时,则清理掉数组首地址的元素,且剩余需整体需整体向前挪一位,时间复杂度为O(n)。(优化:清理的时候可以考虑一次性清理一定数量,从而降低清理次数,提高性能)。
2.如何通过单链表实现“判断某个字符串是否为水仙花字符串”?
(1).前提:字符串以单个字符的形式存储于单链表中。
(2).遍历链表,判断字符个数是否为奇数,若为偶数则不是。
(3).将链表中的字符倒序存储一份在另一个链表中。
(4).同步遍历两个链表,比较对应的字符是否相等,若相等,则是水仙花字符串,若不相等,那么其不是水仙花字符串。
*六、设计思想
时空替换思想:“用空间换时间”与“用时间换空间”
当内存空间充足的时候,如果我们更加追求代码的执行速度,那么我们就可以选择空间复杂度相对较高,时间复杂度相对较低的算法和数据结构,缓存就是空间换时间的例子。反而如果内存比较紧缺,比如代码跑在手机或者单片机上,这时,就要反过来用时间换空间的思路。

3.队列

什么是队列?
队列是一种受限的线性表数据结构,只支持两种操作:入栈Push()和出栈popo(),队列与数组非常相似,支持的操作也相似,很有限,最基本的操作也是两个:入队enqueue(),放一个数据到队列尾部;出列dequeue(),下图是从队列列头取一个元素在这里插入图片描述
特点:
1.队列和栈一样都是一种抽象的数据结构。
2.具有先进先出的特性,支持在队尾插入元素,在队头删除元素
实现:
队列可以用数组来实现,也可以用链表来实现
用数组实现的队列叫做顺序对列,用链表实现的队列叫做链式对列。
基于数组的对列:
实现的思路:
实现队列需要两个指针:一个是head指针,指向队头;一个是tail指针,指向队尾。
可以用下面这幅图来理解,当a,b,c,d依次入队之后,队列中的head指针指向下标为0的位置,tail指针指向下标为4的位置。在这里插入图片描述
当我们调用两次出队操作之后,队列中head指针指向下标为2的位置,tail指针仍然指向下标为4的位置。
随着不停地进行入队、出队操作,head和tail都会持续往后移动。当tail移动到最右边,即使数组中还有空闲空间,也无法继续往队列中再添加数据了。所以,这个问题我们应该如何解决呢?
在出队时可以不用搬移数据。如果没有空闲空间了,我们只需要在入队时,再集再触,发生一次搬移操作。
当队列的tail指针移动到数组的最右边后,如果有新的数据入队,我们可以将head到tail之间的所有数据。整体地搬移到数组中0到tail-head的位置
在这里插入图片描述
基于链表的实现:
需要两个指针:head指针和tail指针,它们分别指向链表的第一个结点和最后一个结点。
如下图所示,入队时,tail->next=new node,tail=tail->next;
在这里插入图片描述
循环队列;
我们刚才用数组来实现队列的时候,在tail==n时,会有数据搬移操作,这样入队操作性能就会受到影响,那有没有办法能够避免数据搬移操作呢?我们来看看循环队列的解决思路。顾名思义,它的形状像一个环,原本数组是有头有尾的,是一条直线。现在我们把首尾相连,变成了一个环,如图所示:
在这里插入图片描述

我们可以看到,图中这个队列的大小为8,当前head-4,tail-7,当有一个新的元素a入队时,我们放入下标为7的位置。但这个时候我们并不把tail更新为8,而是将其在环中后移一位,到下标为0的位置。当再有一个元素b入队时,我们将b放入下标为0的位置,然后tail加1更新为1,所以,在a,b依次入队之后,循环队列中的元素就变成了下面的样子:在这里插入图片描述
队列为空的判断条件是head==tail,但队列满的判定条件就稍微有点复杂了。下面是一张队列满的图,试着从中总结一下规律:
在这里插入图片描述
就像我图中画的队满的情况,tail=3,head-4,n=8,所以总结一下规律就是(3+1)%8-4,多画几张队满的图,你就会发现当队满时(tail+1)%n=head。我们还可以发现,当队列满时,图中的tail指向的位置是没有存储数据的。所以,循环队列会浪费一个数组的存储空间。而解决浪费一个存储空间的思路:定义一个记录队列大小的值size,当这个值与数组大小相等时,表示队列已满,当tail达到最底时,size不等于数组大小时,tail就指向数组第一个位置。当出队时:size–,入队时:size++;

阻塞队列和并发队列
阻塞对列其实就是在队列基础上增加了阻塞操作。
简单地来说,就是在队列为空的时候,数据会被堵塞。因为此时还没有数据可取,直到队列中有了数据才能返回;如果队列已经满了,那么插入数据的操作就会被阻塞,直到队列中有空闲位置后再插入数据,然后再返回。
在这里插入图片描述
你应该已经发现了,上述的定义就是一个“生产者-消费者模型”!是的,我们可以使用阻塞队列,轻松实现一个“生产者-消费者模型”!这种基于阻塞队列实现的“生产者-消费者模型”,可以有效地协调生产和消费的速度,当“生产者”生产数据的速度过快,“消费者来不及消费时,存储数据的队列很快就会满了。这个时候,生产者就会阻塞等待,直到“消费者”消费了数据,“生产者”才会被唤醒继续生产,不仅如此,基于阻塞队列,我们还可以通过协调“生产者”和“消费者”的个数,来提高数据的处理效率。比如前面的例子,我们可以多配置几个“消费者”,来应对一个“生产者”。
小结:
队列最大的特点就是先进先出,主要的两个操作就是入队和出队。
它既可以用数组来实现,也可以用链表来实现,用数组实现的叫做顺序队列,用链表实现的叫做链式队列,在用数组实现队列的时候,会有数据搬移操作,要想解决数据搬移的问题,我们就需要一个像环一样的循环队列,如果要想写出没有bug的循环队列实现代码,关键是确定好队空和队满的判定条件。、
阻塞队列、并发队列,底层都还是队列这种数据结构,只不过在之上附加了很多功能。阻塞队列就是入队、出队操作可以阻塞,并发队列就是队列的操作多线程安全。

4.递归算法

一、什么是递归
1.递归是一种非常高效、简洁的编码技巧,一种应用非常广泛的算法,比如DFS深度优先搜索、前中后序二叉树遍历等都是使用递归。
2.方法或函数调用自身的方式称为递归调用,调用称为调,返回称为归。
3.基本上,所有的递归问题都可以用递推公式来表示,比如
f(n)=f(n-1)+1;
f(n)=f(n-1)+f(n-2);
f(n)=n*f(n-1);
二、为什么使用递归?递归的优缺点?
1.优点:代码的表达力很强,写起来简洁;
2.缺点:空间复杂度高、有堆栈溢出风险、存在重复计算、过多的函数调用会耗时较多的问题。
三、什么样的问题可以使用递归解决呢?
一个问题只要同时满足以下三个条件,就可以使用递归来解决:
1.问题的解可以分解为几个子问题的解。何为子问题?就是数据规模更小的问题。
2.问题与子问题,除了数据规模的不同,求解思路完全一样。
3.存在递归终止条件。
**

四、如何实现递归

1.递归代码的编写
写递归代码的关键就是找到如何将大问题分解为小问题的规律,并且基于此写出递推公式,然后再推敲终止条件,最后将递推公式翻译成代码。
2.递归代码的理解
对于递归代码,若试图想清楚整个递和归的过程,实际上是进入了一个思维的误区。
那该如何理解递归代码呢?如果一个问题A可以分解为若干个子问题B、C、D,你可以假设子问题B、C、D已经解决。而且,你只需要思考问题A与子问题B、C、D两层的关系即可,不需要一层层往下思考子问题与子子问题、子子问题与子子子问题之间的关系,屏蔽掉递归细节,这样子理解起来就简单多了。
因此,理解递归代码,就把它抽象为一个递推公式,不用想一层层的调用关系,不要试图用人脑去分析递归的每个步骤。

递归的关键是终止条件。

五、递归常见问题及解决方案

1.警惕堆栈溢出:可以声明一个全局变量来控制递归的深度,从而避免堆栈溢出。

2…警惕重复计算:通过某种数据结构来保存已经求解过的值,从而避免重复计算。
六、如何将递归代码改写成非递归代码?
笼统地来说,所有的递归代码都可以改写为迭代循环的非递归写法,如何做?抽象出递推公式、初始值和边界条件,然后用迭代循环实现。

5、排序

一、排序方法与复杂度归类
(1)几种最经典、最常用的排序方法:冒泡排序、插入排序、选择排序、快速排序、归并排序、计数排序、基数排序、桶排序。
(2)复杂度归类
冒泡排序、插入排序、选择排序 O(n^2)
快速排序、归并排序 O(nlogn)
计数排序、基数排序、桶排序 O(n)

二、如何分析一个“排序算法”?
<1>算法的执行效率

  1. 最好、最坏、平均情况时间复杂度。
  2. 时间复杂度的系数、常数和低阶。
  3. 比较次数,交换(或移动)次数。
    <2>排序算法的稳定性
  4. 稳定性概念:如果待排序的序列中存在值相等的元素,经过排序之后,相等元素之间原有的先后顺序不变。
  5. 稳定性重要性:可针对对象的多种属性进行有优先级的排序。
  6. 举例:给电商交易系统中的“订单”排序,按照金额大小对订单数据排序,对于相同金额的订单以下单时间早晚排序。用稳定排序算法可简洁地解决。先按照下单时间给订单排序,排序完成后用稳定排序算法按照订单金额重新排序。
    <3>.排序算法的内存损耗
    原地排序算法:特指空间复杂度为O(1)的排序算法。
    常见的排序算法:

在这里插入图片描述
在这里插入图片描述
冒泡排序只会操作相邻的两个数据。每次冒泡操作都会对相邻的两个元素进行比较,看是否满足大小关系要求,如果不满足就让它们两个互换。
代码:

1.public int[] bubbleSort(int [a])
{
int n=a.length;
if(n<=1)
{
return a;
}
for(int i=0;i<n;i++)//提前退出冒泡循环的标志
{
boolean flag =false;
for(int j=0;j<n-i-1;j++)
{
if(a[j]>a[j+1])
{
int temp=a[j];
a[j]=a[j+1];
a[j+1]=temp;

flag=true;
}
if(!flag)
{
break;
}
}
}
return a;
}

四、插入排序

在这里插入图片描述
插入排序将数据分成已排序区间和未排序区间。初始已排序区间只有一个元素,即数组第一个元素。在未排序区间取出一个元素插入到已排序区间的合适位置,直到未排序区间为空。
代码:

1.public int[] insertionSort(int [a])
{
int n=a.length;
if(n<1)
{
return a;
}
for(int i=1;i<n;i++)
{
int value =a[i];
int j=i-1;
for( ;j>=0;j--)
{
if(a[j]>value)
{
a[j+1]=a[j];
}
else
{
break;
}
a[j+1]=value;
}
return a;
}

五、选择排序
在这里插入图片描述
选择排序将数组分成已排序区间和未排序区间。初始已排序区间为空。每次从未排序区间中选出最小的元素插入到已排序区间的末尾,直到未排序区间为空。
代码:

1.public int[] selectionSort(int [a])
{
int n=a.length;
for(int i=0;i<a.length-1;i++)
{
for(int j=i+1;j<a.length;j++)
{
if(a[i]>a[j])
{
int temp=a[i];
a[i]=a[j];
a[j]=temp;
}
}
}
return a;
}
`
``
**六、归并排序**
如果要排序一个数组,我们先把数组从中间分成前后两部分,然后对前后两部分分别排序,再将排好序的两部分合并在一起,这样整个数组就都有序了。

![在这里插入图片描述](https://img-blog.csdnimg.cn/20210205100400318.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L20wXzUyMzk3NzE1,size_16,color_FFFFFF,t_70)
实现思路:

![在这里插入图片描述](https://img-blog.csdnimg.cn/2021020510045031.png)
merge-sort(p....r)表示,给下标从p到r之间的数组排序。我们将这个排序问题转化为两个子问题,merge-sort(p...q)和merge-sort(q+1..r),其中下标q等于p和r的中间位置。也就是说,(p+r)/2,当下标从P到q和从q+1到r这两个子数组都排好序之后,我们再将两个有序的子数组合并在一起,这样下标从p到r之间的数据也排好序了。
代码:

```c
1.//归并排序算法,a是数组,n表示数组大小
2.public static void mergeSort(int [a],int n)
3.{
4.mergeSortInternally(a,0,n-1);
5.}
6.//递归调用函数
7.private static void mergeSortInternally(int [a],int p;int r);
8.{
9.if(p>=r)
10.return;//递归终止条件
11.int q= (p+r)/2;//取p到r之间的中间位置q
12.mergeSortInternally(a,p,q);
13.mergeSortInternally(a,q+1,r);
14.//将A[p....q]和A[q+1...r]合并为A[p...r]
15.merge(a,p,q,r);
16.}
17.private static void merge(int [a],int p,int q,int r);
18.{
19.int i=p;
20.int j=q+1;
21.int k=0;
22.int [temp]=new int [r-p+1];//申请一个大小与a[p..r]一样的临时数组
23.//排序
24.while(i<=q&&j<=r)
25.{
26.if(a[i]<=a[j])
27.{
28.temp[k++]=a[i++];
29.}
30.else
31.{
32.temp[k++]=a[j++];
33.}
34.}
35.//判断哪一个子数组中有剩余的数据
36.int start = i;
37.int end = q;
38.if(j<=r)
39.{
40.start=j;
41.end=r;
42.}
43.while(start<=end)
44.{
45.for(i=0;i<=r-q;++i)
46.{
47.a[q+i]=tmp[i];
48.}
49.}
merge是这样执行的。

在这里插入图片描述
代码分析:
在这里插入图片描述
在这里插入图片描述
七、快速排序
快排的思想: 如果要排序数组从p到r之间的一组数据,我们选择p到r之间的任意一个数据作为pivot(分区点)。我们遍历p到r之间的数据,将小于pivot的放到左边,将大于pivot的数据放在其右边,将pivot放到中间。经过这一步骤之后,数组p到r之间的数据就被分成了三个部分,前面p到q-1之间的都是小于pivot的,中间是pivot,后面的q+1到r之间是大于pivot的。

快排利用的是分而治之的思想

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
八、线性排序
时间复杂度O(n)

我们把时间复杂度是时间的排序算法叫做线性排序(Linear sort)常见的线性算法有: 桶排序、计数排序、基数排序
特点:
非基于比较的排序算法

桶排序
桶排序,顾名思义,会用到“桶”,核心思想就是将要排序的数据分到几个有序的桶里,每个桶里的数据再单独进行排序。桶内排完序之后,再把每个桶里的数据按照顺序依次取出,组成的序列就是有序的了。

对排序的数据要求苛刻:

1.要排序的数据需要很容易就能划分成m个桶,并且,桶与桶之间有着天然的大小顺序。
2.数据在各个桶之间的分布是比较均匀的。
3.桶排序比较适合用在外部排序中。所谓的外部排序就是数据存储在外部磁盘中,数据量比较大,内存有限,无法将数据全部加载到内存中。
在这里插入图片描述
计数排序
计数排序只能用在数据范围不大的场景中,如果数据范围k比要排序的数据n大很多,就不适合用计数排序了。

计数排序只能给非负整数排序,如果要排序的数据是其他类型的,要将其在不改变相对大小的情况下,转化为非负整数。
代码:

 // 计数排序,a是数组,n是数组大小。假设数组中存储的都是非负整数。
1.	  public static void countingSort(int[] a) {
2.		int n = a.length;
3.	    if (n <= 1) return;
4.	 
5.	    // 查找数组中数据的范围
6.	    int max = a[0];
7.	    for (int i = 1; i < n; ++i) {
8.	      if (max < a[i]) {
9.	        max = a[i];
10.	      }
11.	    }
12.	 
13.	    // 申请一个计数数组c,下标大小[0,max]
14.	    int[] c = new int[max + 1];
15.	    for (int i = 0; i < max + 1; ++i) {
16.	      c[i] = 0;
17.	    }
18.	 
19.	    // 计算每个元素的个数,放入c中
20.	    for (int i = 0; i < n; ++i) {
21.	      c[a[i]]++;
22.	    }
23.	 
24.	    // 依次累加
25.	    for (int i = 1; i < max + 1; ++i) {
26.	      c[i] = c[i-1] + c[i];
27.	    }
28.	 
29.	    // 临时数组r,存储排序之后的结果
30.	    int[] r = new int[n];
31.	    // 计算排序的关键步骤了,有点难理解
32.	    for (int i = n - 1; i >= 0; --i) {
33.	      int index = c[a[i]]-1;
34.	      r[index] = a[i];
35.	      c[a[i]]--;
36.	    }
37.	 
38.	    // 将结果拷贝会a数组
39.	    for (int i = 0; i < n; ++i) {
40.	      a[i] = r[i];
41.	    }
42.	  }

散列表

什么是散列表:
散列表用的是数组支持按照下标随机访问数据的特性,所以散列表其实就是数组的一种扩展,由数组演化而来。可以说,如果没有数组,就没有散列表。
原理:
散列表用的就是数组支持按照下标随机访问的时候,时间复杂度为O(1)的特性。我们通过散列函数把元素的键值映射为下标,然后将数据存储在数组对应下标的位置。当我们按照键值查询元素的时候,我们用同样的散列函数,将键值转换为数组下标,从对应的数组下标的位置取数据。
散列函数的设计要求:
1.散列函数计算得到的散列值是一个非负整数。
2.如果key1=key2,那么hash(key1)==hash(key2);
3.如果key1!=key2,a那么hash(key1)!=hash(key2);
散列函数的设计不能太复杂,散列函数生成值要尽可能随机并且均匀分布。
如果不符合那么就会出现散列冲突,散列冲突是无法避免的,
解决散列冲突的方法有两种:
开放寻址法(open addressing)和链表法(chaining)
开放寻址法:如果出现了散列冲突,我们就重新探测一个空闲位置,将其插入。
装在因子:散列表中一定比例的空闲槽位。公式:散列表的装载因子=填入表的元素个数/散列表的长度
装载因子越大,说明空闲位置越少,冲突越多,散列表的性能会下降。
链表法:
链表法是一种更加常用的散列表冲突解决办法,相比于开放寻址方法,它要简单很多。我们来看这个图,在散列表中,每个“桶”或者“槽”会对应一条链表,所有散列值相同的元素我们都放到相同槽位对应的链表中。
在这里插入图片描述

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值