关于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" -> "\t""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" -> "\t""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函数会不断地交换元素的位置,所以它肯定会改变数据输入的顺序;既然要交换元素的位置,那么所有元素必须要读到内存空间中,所以它会占用比较大的空间,至少能容纳整个数组;数据越多,占用的空间必然越大,海量数据处理起来相对吃力。

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

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

                                   <未完>

posted @ 2018-09-29 19:55  IT晓白  阅读(153)  评论(0编辑  收藏  举报