• 关于Hadoop的杂乱无章(续更)


    JPS(是jdk的工具):表示查看当前主机有哪些运行的进程
    NameNode :表示主节点
    DataNode:表示数据节点
    SecondaryNameNode :表示次要名称节点
    --节点表示:一台机器
    进程是运行在机器上的,一个软件可以有多个进程(分布式软件:Hadoop)
    HDFS只是Hadoop的一部分,Hadoop还有MR、yarn
    HDFS是分布式软件系统:将文件自动分布在三台机器上(副本/备份)
    HDFS的特点:
    1.可容错:表示你把软件删除了,还可以复原
    2,低廉硬件(安装了x86服务器/CPU)
    3.高吞吐量(IO):分布多台机器同时处理
    4.适用于有超大数据集的应用程序(1G以上大小的表格)
    --但是不能满足:update(随机读写),但是可以以流的形式访问文件,提高了性能
    ---------
    POSIX:可移植操作系统接口(是一个规范)
    说白就是一个文件系统的接口,ext4、ext3、FAT32、NTFS全部都实现了这个接口,但是功能不一定全部实现
    -HDFS框架是有中心的架构
    主节点是:NameNode
    用于对外(client)通信,通过交换机接收和发送,但是查询到的数据不经过这里直接由DataNode交给client
    管理+协调,可以控制其他节点
    完成任务(干活节点):DataNode
    DataNode之间没有关联,之间的通信也是通过局域网
    --实际生产环境中是有多个NameNode
    MateData(元数据):元数据就是形容数据的数据
    在这里他包含NameSpace:目录结构+blockdata(块数据)
    HDFS是分块存储的,块表示一个文件存储在哪台机器上
    ------------
    client(客户端)与NameNode之间是元数据交换
    client与DataNode之间是数据交换
    在同一个机架上通信快,HDFS会在同一个机架上放两个数据(在两台机器上),在不同的机架上保存一个(备份)
    128M是一个block(块)
    block就是键值对--映射到内存中就是元数据
    key:block的id(哪台机器)-----------value:block的内容地址
    --文件越小消耗的内存越大,因为保存相同大小,文件越多分的块越多,映射到内存中的元数据越多
    例如:文件1024G,保存1T大小的这样的文件,占用内存8M,磁盘大小是3T
        文件1M,保存1pb大小的这样的文件,占用内存1T,磁盘大小是3pb
    分块是客户端
    客户端将文件分块,串行依次写入,分的块不一定保存在同一台机器上
    1.向文件系统(Hadoop集群)中上传文件
    hadoop fs -put /abc /
    /abc:表示将要上传的文件
    /:表示上传到集群中的路径是:/
    ----------
    Fs文件系统----保存file
    数据库(DB)----保存表格/table-----存入之后需要经常修改
    HDFS---file----crd(没有u不能随机读写,但是能追加),
    记住HDFS与数据库无关,HDFS是处理海量数据的,不能经常修改
    ---
    OLTP:在线事务处理(对数据库的写操作)
    主要是数据库操作--web网站
    特点:实时(立即有效果),处理的数据量小
    OLAP:对数据库的读、分析处理
    主要是:Hadoop
    特点:实时不高,数据量大,HDFS是一次写入多次读取的模式
    --元数据:ns(目录结构)+blockMap(文件所在的地址)
    Secondary不是NameNode的备份,因为NameNode与Secondary共存亡
    他类似于秘书:将内存中的内容持久化
    工作:将最近的一个image(存量)与edit—log(增量)合并--生成出最新的image,并将开始的image删除,循环操作
    这是因为每写一个文件,都会产生一个edit-log
    过程:NameNode将日志+最新的image发送给--Secondary,Secondary将这两个和合并,并删除原来的image,发送给NameNode一个最新的image(这两个是进程之间通信通过Http协议)
    --Hadoop中的配置文件
    permission权限,false:表示任何人都可以访问,实际生产环境中true
    每次格式化,version中的NameSpaceID、clusterID(集群的id)改变了
    集群在开启前几分钟会开启安全模式(SafeMode),DataNode向NameNode汇报数据信息,之后自动关闭
    50010是集群之间的通信端口号,如果是非知名端口号,防火墙会拦截
    hadoop fs -checksum /fileName:校验码,可以查看文件是否被改变(是否成为脏数据)

    ------mapreduce

    hdfs:是分布式存储,可以单独使用

    mapreduce是分布式计算,必须按照分布式存储才能分布式计算

    map:映射、转化、分

    reduce:合并、减少

    MR是计算模型:可以并行化处理大量数据(提高效率)

    a.并行(事):一件事分成多个快,多个人同时做

    b.并发(人):多个人同时做一件事

    cdh与hdp是两家最大的hadoop上市公司(在2018年听说要合并)

    以后spark(函数式设计语言)会代替mapreduce,因为mapreduce相对于比较慢、难以维护。

    -------实际生产中的集群需求:

    MapReduce集群搭建

    1.yarn环境

    在/usr/local/hadoop/etc/hadoop/yarn-env.sh中配置--java安装路径

    2.MapReduce配置 IP:8088

    将mapred-site.xml.tmplate复制成mapred-site.xml

    cp mapred-site.xml.tmplate mapred-site.xml

    将mapred-site.xml中添加配置

    <property>

            <name>mapreduce.framework.name</name>

           <value>yarn</value>

    </property>

    <property>

           <name>mapreduce.jobhistory.address</name>

            <value>master:10020</value>

    </property>

    <property>

            <name>mapreduce.jobhistory.webapp.address</name>

            <value>master:19888</value>

    </property>

    3.yarn配置

    在yarn-site.xml中添加配置

    <property>

            <name>yarn.resourcemanager.hostname</name>

            <value>master</value>

    这里表示yarn存放在master,这里value中的值填什么yarn就在那里

    </property>

    <property>

            <name>yarn.nodemanager.aux-services</name>

            <value>mapreduce_shuffle</value>

    </property>

    --如果不是单机伪分布集群则处理一下操作

    4.将etc/hadoop复制到其他节点

    5.最终启动

    启动

    执行start-yarn.sh命令(在这之前确保HDFS已经启动,没有启动的先start-dfs.sh)。

    执行成功后,通过JPS检查ResourceManager、NodeManager是否启动。

    如果启动成功,通过master:8088可以打开MapReduce“应用”站点:

    注意:需要配置本地Windows系统的hosts文件才能在本地使用主机名!

    phoenix在测试javaAPI的注意: 

    (1)插入值如果是字符串要用单引号引起来,切记不能用双引号!!! 
    (2)表名如果要体现小写效果,必须要用双引号!!!


    upsert into "person" values (1, 'test', 100);        正确
    upsert into "person" values (1, "test", 100);        错误


    HBase的协处理器

    过程:

    Java写协处理器代码-------打jar包-------上传到hbase/lib目录下-----重启hbase(为了加载jar包)-------在hbase上创建表----为表添加自定义的协处理器(alter  ‘tableName’,’coprocessor’ => ‘|classPath|‘)------测试:put数据。

    类:将索引放在另一张表中,表也不放在hbase上,放在sole上。

    1. EndPoint:数据存储过程(hbase的协处理器—现在已经不常用)

    存储在mysql中的多条sql语句的集合,执行多条语句,提高性能

    但是最致命的缺点是:迁移性差(写的越多,迁移的时候要写的sql语句越多)

     

    b. Observer(触发器):就是在写入数据之前,为表创建二级索引

    DML:观察表数据的变化—CURD(增删改查)-----在Observer中RegionObserver

    DDL:观察表的元数据变化---创表删表之类-------在Observer中MasterObserver

    WAL:写之前记录日志

     

    索引:排序之后的文件---有索引,先读取索引文件,再查询数据内容。(不用全表扫描)

    覆盖索引(非主键的索引):增大索引文件,但是读取数据快,也会稍微降低写入数据的速度(以空间换时间)

    Hbase本身只有rowKey一个索引。

    可以利用phoenix插件可以随意创建二级索引,提高hbase的查询速度;

    也可以在写入数据之前对表添加协处理器。


    Hbase RowKey设计:

    1. rowKey的大小:不是很大

    长度不宜过长:<=256字节

    1. rowKey与业务整合:

    因为hbase只有rowKey一个索引,创建的时候尽量与业务逻辑整合(就是查询的条件放在rowKey中);为了命中索引,提高查询速度。

    1. rowKey散列:为了避免写热点

    写热点:数据倾斜(数据分布不均匀)

    1. 加随机前缀(不建议使用)--因为需要另保存前缀用来查询数据
    2. 加hash前缀(建议使用)--由hash算法就可以取前缀值,用来查询
    3. 倒叙写入(偷懒式)

    比如:1-2018-start--------------àtrats-8102-1

    先构造rowKey,再倒序。

           正序可能(数据)递增,而导致写热点。

    ----------libreoffice

    下载:

    Linux centos

    https://donate.libreoffice.org/zh-CN/dl/rpm-x86_64/6.0.6/zh-CN/LibreOffice_6.0.6_Linux_x86-64_rpm.tar.gz

    安装:

    1.解压文件

    2.进入到的RPMS目录

    3.yum localinstall *.rpm

    文档转换:

    word→pdf

    libreoffice6.0 --invisible --convert-to pdf some.doc

    word→html
    libreoffice6.0 --invisible --convert-to html some.doc

    或者

    soffice --convert-to pdf somedoc

    转换(word-->pdf)出现下面错误,请下载插件

    yum -y install ibus

    [root@master209 local]# libreoffice --invisible --convert-to pdf 1.doc
    -bash: libreoffice: command not found
    [root@master209 local]# libreoffice6.0 --invisible --convert-to pdf 1.doc
    /opt/libreoffice6.0/program/soffice.bin: error while loading shared libraries: libcairo.so.2: cannot open shared object file: No such file or directory

    异常处理:
    word->pdf时中文乱码

    1.打开c盘下的Windows/Fonts目录

    2.在这之前我们还需要新建目录,首先在/usr/shared/fonts目录下新建一个目录chinese

    3.然后就是将上面的两个字体上传至/usr/share/fonts/chinese目录下即可

    chmod -R 755 /usr/share/fonts/chinese

    4.刷新内存中的字体缓存,这样就不用reboot重启了,输入:fc-cache

    5.最后再次通过fc-list看一下字体列表:(ps可能我添加的比较多)

    卸载libreoffice

    yum erase libreoffice*

    --------------kafka

    Kafka 消息队列、消息系统、消息中间件、消息的推送口

    生产者-----à(中介)ß-------消费者

    中间通过TCP通信

    中间件的演化:

    类---à软件

    线程-à进程

    Flum中agent表示一个节点(进程)

    Kafka中broker表示一个节点(进程)

    --特性:分布、可分区、可复制(备份)、顺序读写-速度高

    Offset:消费者的(每个分区的)偏移量

    Offset是有消费者来维护

    Topic:话题—默认是hash分区

    每个topic的领导者是相对的

    一条数据(一个分区)可以由不同组的多个消费者来消费

    有两种模式:排队、订阅

    消息队列的种类:

    1.ActiceMQ   java

    2.zero MQ 

    3.Rabbit MQ

    4. Rocket MQ   阿里云开元

    5.kafka

    以上都是消息队列,都可以相互替换,一般2,3不常用—使用消息队列就是为了解耦

    一个分区,一个组 能保证有序性

    在0.11之前kafka将元数据保存在zookeeper中

    以日志的形式将数据持久化

    分区个数 按照kafka集群个数的10倍关系

     

    Kafka中的相关配置

    1. acks

    设置为all:安全,可能数据重复,先复制,再告诉

    à-1  提高性能,但是不安全

    à1  数据在leader接收之后,立即回复,如果中介机器宕机,可能丢失

    16kb----批量发送的单位

    Batch(缓冲区-一次发送的数据)与linger(间隔)满足之一、都会发送

    Kafka---à发送的是json字符串

    Enable.auto.commit--àtrue    自动提交

    消费者取出之后就告诉别人消费过了,消费者还没有消费就告诉,如果中间shutdown了,数据就丢失。

    防止数据重复与数据丢失---偏移量问题

    幂等(事务控制)+手动

    Auto.offset.reset---àearliest(最早)  有--(上次)   无---(从头)

                   -àbest (最近)   数据丢失

                   -ànone   测试用

    ---------------------spark

    spark.streaming._    实时处理数据   

    localhost:4040

    storm与stream的区别:

    storm:每次处理一条数据,快,但是处理的数据小(吞吐量小)

    stream:每次可以快速处理一批数据,特征:high-throughput(高吞吐)、fault-tolerant(可容错)、scalable(可扩展)

    配置信息:

    streaming至少两个线程

    new SparkConf().setMaster("local[*]").setAppName("streaming")

    时间用于分割数据:Seconds(5)---5秒

    new StreamingContext(conf,Seconds(5))

     

    数据的来源是通过tcp通信获取Datastream

    Datastream:表示一系列的rdd

    Dstreammap,就是对每个rddmap

    模拟从tcp端口读取数据

    val ds=ssc.socketTextStream("localhost",999)

     

    //启动streaming context,防止没有数据关闭

        //如果没有接受导数据,也不会立刻关闭,会尝试一段时间强制关闭

        ssc.start()

        ssc.awaitTermination()

    --------------------------mobaxterm配置

    Settings

        Configuration

          Terminal

            勾选 Paste using right-click(左键选取,松开左键复制,右键粘贴, 可以使用Ctrl+右键来使用右键功能)

          SSH

            取消勾选 Automatically switch to SSH-browser tab after login(登录后自动切换到SFTP浏览器)

            勾选 SSH keepalive(保持心跳连接不断)

    ------

    命令  hadoop dfsadmin -safemode get  查看安全模式状态

    命令  hadoop dfsadmin -safemode enter    进入安全模式状态

    命令   hadoop dfsadmin -safemode leave   离开安全模式

    --------------spark中rdd、dataframe、dataset联系与区别

    在spark中,RDD、DataFrame、Dataset是最常用的数据类型,本博文给出笔者在使用的过程中体会到的区别和各自的优势

    共性:

    1、RDD、DataFrame、Dataset全都是spark平台下的分布式弹性数据集,为处理超大型数据提供便利

    2、三者都有惰性机制,在进行创建、转换,如map方法时,不会立即执行,只有在遇到Action如foreach时,三者才会开始遍历运算,极端情况下,如果代码里面有创建、转换,但是后面没有在Action中使用对应的结果,在执行时会被直接跳过,如

    1

    2

    3

    4

    5

    6

    7

    8

    val sparkconf = new SparkConf().setMaster("local").setAppName("test").set("spark.port.maxRetries","1000")

    val spark = SparkSession.builder().config(sparkconf).getOrCreate()

    val rdd=spark.sparkContext.parallelize(Seq(("a"1), ("b"1), ("a"1)))

    rdd.map{line=>

      println("运行")

      line._1

    }

    map中的println("运行")并不会运行

    3、三者都会根据spark的内存情况自动缓存运算,这样即使数据量很大,也不用担心会内存溢出

    4、三者都有partition的概念,如

    1

    2

    3

    4

    5

    6

    7

    8

    var predata=data.repartition(24).mapPartitions{

          PartLine => {

            PartLine.map{

              line =>

                 println(“转换操作”)

                                }

                             }

    这样对每一个分区进行操作时,就跟在操作数组一样,不但数据量比较小,而且可以方便的将map中的运算结果拿出来,如果直接用map,map中对外面的操作是无效的,如

    1

    2

    3

    4

    5

    6

    7

    8

    9

    10

    11

    12

    13

    14

    15

    16

    17

    18

    19

    20

    val rdd=spark.sparkContext.parallelize(Seq(("a"1), ("b"1), ("a"1)))

        var flag=0

        val test=rdd.map{line=>

          println("运行")

          flag+=1

          println(flag)

          line._1

        }

    println(test.count)

    println(flag)

        /**

        运行

        1

        运行

        2

        运行

        3

        3

        0

       * */

    不使用partition时,对map之外的操作无法对map之外的变量造成影响

    5、三者有许多共同的函数,如filter,排序等

    6、在对DataFrame和Dataset进行操作许多操作都需要这个包进行支持

    1

    2

    import spark.implicits._

    //这里的spark是SparkSession的变量名

    7、DataFrame和Dataset均可使用模式匹配获取各个字段的值和类型

    DataFrame:

    1

    2

    3

    4

    5

    6

    7

    testDF.map{

          case Row(col1:String,col2:Int)=>

            println(col1);println(col2)

            col1

          case _=>

            ""

        }

    为了提高稳健性,最好后面有一个_通配操作,这里提供了DataFrame一个解析字段的方法

    Dataset:

    1

    2

    3

    4

    5

    6

    7

    8

    case class Coltest(col1:String,col2:Int)extends Serializable //定义字段名和类型

        testDS.map{

          case Coltest(col1:String,col2:Int)=>

            println(col1);println(col2)

            col1

          case _=>

            ""

        }

      

    区别:

    RDD:

    1、RDD一般和spark mlib同时使用

    2、RDD不支持sparksql操作

    DataFrame:

    1、与RDD和Dataset不同,DataFrame每一行的类型固定为Row,只有通过解析才能获取各个字段的值,如

    1

    2

    3

    4

    5

    testDF.foreach{

      line =>

        val col1=line.getAs[String]("col1")

        val col2=line.getAs[String]("col2")

    }

    每一列的值没法直接访问

    2、DataFrame与Dataset一般与spark ml同时使用

    3、DataFrame与Dataset均支持sparksql的操作,比如select,groupby之类,还能注册临时表/视窗,进行sql语句操作,如

    1

    2

    dataDF.createOrReplaceTempView("tmp")

    spark.sql("select  ROW,DATE from tmp where DATE is not null order by DATE").show(100,false)

    4、DataFrame与Dataset支持一些特别方便的保存方式,比如保存成csv,可以带上表头,这样每一列的字段名一目了然

    1

    2

    3

    4

    5

    6

    //保存

    val saveoptions = Map("header" -> "true""delimiter" -> " ""path" -> "hdfs://172.xx.xx.xx:9000/test")

    datawDF.write.format("com.databricks.spark.csv").mode(SaveMode.Overwrite).options(saveoptions).save()

    //读取

    val options = Map("header" -> "true""delimiter" -> " ""path" -> "hdfs://172.xx.xx.xx:9000/test")

    val datarDF= spark.read.options(options).format("com.databricks.spark.csv").load()

    利用这样的保存方式,可以方便的获得字段名和列的对应,而且分隔符(delimiter)可以自由指定

    Dataset:

    这里主要对比Dataset和DataFrame,因为Dataset和DataFrame拥有完全相同的成员函数,区别只是每一行的数据类型不同

    DataFrame也可以叫Dataset[Row],每一行的类型是Row,不解析,每一行究竟有哪些字段,各个字段又是什么类型都无从得知,只能用上面提到的getAS方法或者共性中的第七条提到的模式匹配拿出特定字段

    而Dataset中,每一行是什么类型是不一定的,在自定义了case class之后可以很自由的获得每一行的信息

    1

    2

    3

    4

    5

    6

    7

    8

    9

    10

    11

    12

    13

    14

    15

    case class Coltest(col1:String,col2:Int)extends Serializable //定义字段名和类型

    /**

          rdd

          ("a", 1)

          ("b", 1)

          ("a", 1)

          * */

    val test: Dataset[Coltest]=rdd.map{line=>

          Coltest(line._1,line._2)

        }.toDS

    test.map{

          line=>

            println(line.col1)

            println(line.col2)

        }

    可以看出,Dataset在需要访问列中的某个字段时是非常方便的,然而,如果要写一些适配性很强的函数时,如果使用Dataset,行的类型又不确定,可能是各种case class,无法实现适配,这时候用DataFrame即Dataset[Row]就能比较好的解决问题

    转化:

    RDD、DataFrame、Dataset三者有许多共性,有各自适用的场景常常需要在三者之间转换

    DataFrame/Dataset转RDD:

    这个转换很简单

    1

    2

    val rdd1=testDF.rdd

    val rdd2=testDS.rdd

    RDD转DataFrame:

    1

    2

    3

    4

    import spark.implicits._

    val testDF = rdd.map {line=>

          (line._1,line._2)

        }.toDF("col1","col2")

    一般用元组把一行的数据写在一起,然后在toDF中指定字段名

    RDD转Dataset:

    1

    2

    3

    4

    5

    import spark.implicits._

    case class Coltest(col1:String,col2:Int)extends Serializable //定义字段名和类型

    val testDS = rdd.map {line=>

          Coltest(line._1,line._2)

        }.toDS

    可以注意到,定义每一行的类型(case class)时,已经给出了字段名和类型,后面只要往case class里面添加值即可

    Dataset转DataFrame:

    这个也很简单,因为只是把case class封装成Row

    1

    2

    import spark.implicits._

    val testDF = testDS.toDF

    DataFrame转Dataset:

    1

    2

    3

    import spark.implicits._

    case class Coltest(col1:String,col2:Int)extends Serializable //定义字段名和类型

    val testDS = testDF.as[Coltest]

    这种方法就是在给出每一列的类型后,使用as方法,转成Dataset,这在数据类型是DataFrame又需要针对各个字段处理时极为方便

    特别注意:

    在使用一些特殊的操作时,一定要加上 import spark.implicits._ 不然toDF、toDS无法使用

    ------------hive中的列转行,与行转列

    Hive
    行转列和列转行
    表1:cityInfo


    表2:cityInfoSet

       

    表1和表2的结构如上所示。如何在 hive 中使用 Hql 语句对表1和表2进行互相转化呢?

    行转列
    表1=>表2 可以使用 hive 的内置函数 concat_ws() 和 collect_set()进行转换:

    执行代码如下所示:

    select cityname,concat_ws(',',collect_set(regionname)) as address_set from cityInfo group by cityname;
    1
    列转行
    表2=>表1 可以使用 hive 的内置函数 explode()进行转化。代码如下:

    select cityname, region from cityInfoSet  lateral view explode(split(address_set, ',')) aa as region;

    ------------------spark中的driver与executor

    一、看了很多网上的图,大多是dirver和executor之间的图,都不涉及物理机器

    如下图,本人觉得这些始终有些抽象

    看到这样的图,我很想知道driver program在哪里啊,鬼知道?为此我自己研究了一下,网友大多都说是对的有不同想法的请评论

    二、现在我有三台电脑 分别是

    
     
    1. 192.168.10.82 –>bigdata01.hzjs.co

    2. 192.168.10.83 –>bigdata02.hzjs.co

    3. 192.168.10.84 –>bigdata03.hzjs.co

    集群的slaves文件配置如下:

    
     
    1. bigdata01.hzjs.co

    2. bigdata02.hzjs.co

    3. bigdata03.hzjs.co

    那么这三台机器都是worker节点,本集群是一个完全分布式的集群经过测试,我使用# ./start-all.sh ,那么你在哪台机器上执行的哪台机器就是7071 Master主节点进程的位置,我现在在192.168.10.84使用./start-all.sh 

    那么就会这样

    三、那么我们来看看local模式下 

    现在假设我在192.168.10.84上执行了 bin]# spark-shell 那么就会在192.168.10.84产生一个SparkContext,此时84就是driver,其他woker节点(三台都是)就是产生executor的机器。如图 

    现在假设我在192.168.10.83上执行了 bin]# spark-shell 那么就会在192.168.10.83产生一个SparkContext,此时83就是driver,其他woker节点(三台都是)就是产生executor的机器。如图

    总结:在local模式下  驱动程序driver就是执行了一个Spark Application的main函数和创建Spark Context的进程,它包含了这个application的全部代码。(在那台机器运行了应用的全部代码创建了sparkContext就是drive,也可以说是你提交代码运行的那台机器)

    四、那么看看cluster模式下

    现在假设我在192.168.10.83上执行了 bin]# spark-shell 192.168.10.84:7077 那么就会在192.168.10.84产生一个SparkContext,此时84就是driver,其他woker节点(三台都是)就是产生executor的机器。这里直接指定了主节点driver是哪台机器:如图

    五、如果driver有多个,那么按照上面的规则,去判断具体在哪里

    Driver: 使用Driver这一概念的分布式框架有很多,比如hive,Spark中的Driver即运行Application的main()函数,并且创建SparkContext,创建SparkContext的目的是为了准备Spark应用程序的运行环境,在Spark中由SparkContext负责与ClusterManager通讯,进行资源的申请,任务的分配和监控等。当Executor部分运行完毕后,Driver同时负责将SaprkContext关闭,通常SparkContext代表Driver.

    上面红色框框都属于Driver,运行在Driver端,中间没有框住的部分属于Executor,运行的每个ExecutorBackend进程中。println(pcase.count())collect方法是Spark中Action操作,负责job的触发,因为这里有个sc.runJob()方法

    def count(): Long = sc.runJob(this, Utils.getIteratorSize _).sum

    hbaseRDD.map()属于Transformation操作。

    总结:Spark Application的main方法(于SparkContext相关的代码)运行在Driver上,当用于计算的RDD触发Action动作之后,会提交Job,那么RDD就会向前追溯每一个transformation操作,直到初始的RDD开始,这之间的代码运行在Executor。

    ---------------------------top K

    堆排解法
    用堆排来解决Top K的思路很直接。

    前面已经说过,堆排利用的大(小)顶堆所有子节点元素都比父节点小(大)的性质来实现的,这里故技重施:既然一个大顶堆的顶是最大的元素,那我们要找最小的K个元素,是不是可以先建立一个包含K个元素的堆,然后遍历集合,如果集合的元素比堆顶元素小(说明它目前应该在K个最小之列),那就用该元素来替换堆顶元素,同时维护该堆的性质,那在遍历结束的时候,堆中包含的K个元素是不是就是我们要找的最小的K个元素?

    实现: 
    在堆排的基础上,稍作了修改,buildHeap和heapify函数都是一样的实现,不难理解。

    速记口诀:最小的K个用最大堆,最大的K个用最小堆。

    public class TopK {

        public static void main(String[] args) {
            // TODO Auto-generated method stub
            int[] a = { 1, 17, 3, 4, 5, 6, 7, 16, 9, 10, 11, 12, 13, 14, 15, 8 };
            int[] b = topK(a, 4);
            for (int i = 0; i < b.length; i++) {
                System.out.print(b[i] + ", ");
            }
        }

        public static void heapify(int[] array, int index, int length) {
            int left = index * 2 + 1;
            int right = index * 2 + 2;
            int largest = index;
            if (left < length && array[left] > array[index]) {
                largest = left;
            }
            if (right < length && array[right] > array[largest]) {
                largest = right;
            }
            if (index != largest) {
                swap(array, largest, index);
                heapify(array, largest, length);
            }
        }

        public static void swap(int[] array, int a, int b) {
            int temp = array[a];
            array[a] = array[b];
            array[b] = temp;
        }

        public static void buildHeap(int[] array) {
            int length = array.length;
            for (int i = length / 2 - 1; i >= 0; i--) {
                heapify(array, i, length);
            }
        }

        public static void setTop(int[] array, int top) {
            array[0] = top;
            heapify(array, 0, array.length);
        }

        public static int[] topK(int[] array, int k) {
            int[] top = new int[k];
            for (int i = 0; i < k; i++) {
                top[i] = array[i];
            }
            //先建堆,然后依次比较剩余元素与堆顶元素的大小,比堆顶小的, 说明它应该在堆中出现,则用它来替换掉堆顶元素,然后沉降。
            buildHeap(top);
            for (int j = k; j < array.length; j++) {
                int temp = top[0];
                if (array[j] < temp) {
                    setTop(top, array[j]);
                }
            }
            return top;
        }
    }


    时间复杂度 
    n*logK

    速记:堆排的时间复杂度是n*logn,这里相当于只对前Top K个元素建堆排序,想法不一定对,但一定有助于记忆。

    适用场景 
    实现的过程中,我们先用前K个数建立了一个堆,然后遍历数组来维护这个堆。这种做法带来了三个好处:(1)不会改变数据的输入顺序(按顺序读的);(2)不会占用太多的内存空间(事实上,一次只读入一个数,内存只要求能容纳前K个数即可);(3)由于(2),决定了它特别适合处理海量数据。

    这三点,也决定了它最优的适用场景。

    快排解法
    用快排的思想来解Top K问题,必然要运用到”分治”。

    与快排相比,两者唯一的不同是在对”分治”结果的使用上。我们知道,分治函数会返回一个position,在position左边的数都比第position个数小,在position右边的数都比第position大。我们不妨不断调用分治函数,直到它输出的position = K-1,此时position前面的K个数(0到K-1)就是要找的前K个数。

    实现: 
    “分治”还是原来的那个分治,关键是getTopK的逻辑,务必要结合注释理解透彻,自动动手写写。

    public class TopK {

        public static void main(String[] args) {
            // TODO Auto-generated method stub
            int[] array = { 9, 3, 1, 10, 5, 7, 6, 2, 8, 0 };
            getTopK(array, 4);
            for (int i = 0; i < array.length; i++) {
                System.out.print(array[i] + ", ");
            }
        }

        // 分治
        public static int partition(int[] array, int low, int high) {
            if (array != null && low < high) {
                int flag = array[low];
                while (low < high) {
                    while (low < high && array[high] >= flag) {
                        high--;
                    }
                    array[low] = array[high];
                    while (low < high && array[low] <= flag) {
                        low++;
                    }
                    array[high] = array[low];
                }
                array[low] = flag;
                return low;
            }
            return 0;
        }

        public static void getTopK(int[] array, int k) {
            if (array != null && array.length > 0) {
                int low = 0;
                int high = array.length - 1;
                int index = partition(array, low, high);
                //不断调整分治的位置,直到position = k-1
                while (index != k - 1) {
                    //大了,往前调整
                    if (index > k - 1) {
                        high = index - 1;
                        index = partition(array, low, high);
                    }
                    //小了,往后调整
                    if (index < k - 1) {
                        low = index + 1;
                        index = partition(array, low, high);
                    }
                }
            }
        }
    }

    时间复杂度 
    n

    速记:记住就行,基于partition函数的时间复杂度比较难证明,从来没考过。

    适用场景 
    对照着堆排的解法来看,partition函数会不断地交换元素的位置,所以它肯定会改变数据输入的顺序;既然要交换元素的位置,那么所有元素必须要读到内存空间中,所以它会占用比较大的空间,至少能容纳整个数组;数据越多,占用的空间必然越大,海量数据处理起来相对吃力。

    但是,它的时间复杂度很低,意味着数据量不大时,效率极高。

    好了,两种解法写完了,赶紧实现一下吧。

                                       <未完>

  • 相关阅读:
    nyoj 21三个水杯(BFS + 栈)
    hdu 4493 Tutor
    树的判断(poj nyoj hduoj)
    nyoj 228 士兵杀敌(五)
    poj 3468 A Simple Problem with Integers(线段树)
    hdu 2565 放大的X
    nyoj 528 找球号(三)(哈希)
    nyoj 138 找球号(二)(哈希)
    算法之搜索篇
    每日命令:(11)nl
  • 原文地址:https://www.cnblogs.com/pigdata/p/10305524.html
Copyright © 2020-2023  润新知