zoukankan      html  css  js  c++  java
  • Spark编程基础_RDD编程

    RDD(Resilient Distributed Dataset)叫做弹性分布式数据集,是Spark中最基本的数据抽象,它代表一个不可变、可分区、里面的元素可并行计算的集合。RDD具有数据流模型的特点:自动容错、位置感知性调度和可伸缩性。RDD允许用户在执行多个查询时显式地将工作集缓存在内存中,后续的查询能够重用工作集,这极大地提升了查询速度。
    1 RDD编程基础
    1.1 RDD创建
    1. 从文件系统中加载数据创建RDD
    Spark采用textFile()方法来从文件系统中加载数据创建RDD
    该方法把文件的URI作为参数,这个URI可以是:
    本地文件系统的地址
    或者是分布式文件系统HDFS的地址
    或者是Amazon S3的地址等等
    (1)从本地文件系统中加载数据创建RDD

    scala> val lines = sc.textFile("file:///usr/local/spark/mycode/rdd/word.txt")

    (2)从分布式文件系统HDFS中加载数据

    1 scala> val lines = sc.textFile("hdfs://localhost:9000/user/hadoop/word.txt")
    2 scala> val lines = sc.textFile("/user/hadoop/word.txt")
    3 scala> val lines = sc.textFile("word.txt")

    三条语句是完全等价的,可以使用其中任意一种方式
    2. 通过并行集合(数组)创建RDD
    可以调用SparkContext的parallelize方法,在Driver中一个已经存在的集合(数组)上创建。

    scala>val array = Array(1,2,3,4,5)
    array: Array[Int] = Array(1, 2, 3, 4, 5)
    scala>val rdd = sc.parallelize(array)
    rdd: org.apache.spark.rdd.RDD[Int] = ParallelCollectionRDD[13] at parallelize at <console>:29

    或者,也可以从列表中创建:

    scala>val list = List(1,2,3,4,5)
    list: List[Int] = List(1, 2, 3, 4, 5)
    scala>val rdd = sc.parallelize(list)
    rdd: org.apache.spark.rdd.RDD[Int] = ParallelCollectionRDD[14] at parallelize at <console>:29
    

    1.2 RDD操作

    1. 转换操作

    filter(func) 筛选出满足函数func的元素,并返回一个新的数据集
    map(func) 将每个元素传递到函数func中,并将结果返回为一个新的数据集
    flatMap(func) 与map()相似,但每个输入元素都可以映射到0或多个输出结果
    groupByKey() 应用于(K,V)键值对的数据集时,返回一个新的(K, Iterable)形式的数据集
    reduceByKey(func) 应用于(K,V)键值对的数据集时,返回一个新的(K, V)形式的数据集,其中每个值是将每个key传递到函数func中进行聚合后的结果(统计结果)


    案例:
    filter(func) //筛选出包含Spark的行

    scala> val lines =sc.textFile(file:///usr/local/spark/mycode/rdd/word.txt)
    scala> val linesWithSpark=lines.filter(line => line.contains("Spark")) 
    //另外一个实例,每一行用空格分割
    scala> val lines = sc.textFile("file:///usr/local/spark/mycode/rdd/word.txt")
    scala> val words=lines.map(line => line.split(" "))

    map(func) //map(func)操作将每个元素传递到函数func中,并将结果返回为一个新的数据集

    scala> data=Array(1,2,3,4,5)
    scala> val rdd1= sc.parallelize(data)
    scala> val rdd2=rdd1.map(x=>x+10)

    flatMap(func)

    scala> val lines = sc.textFile("file:///usr/local/spark/mycode/rdd/word.txt")
    scala> val words=lines.flatMap(line => line.split(" "))


    groupByKey()
    groupByKey()应用于(K,V)键值对的数据集时,返回一个新的(K, Iterable)形式的数据集

    reduceByKey(func)
    reduceByKey(func)应用于(K,V)键值对的数据集时,返回一个新的(K, V)形式的数据集,其中的每个值是将每个key传递到函数func中进行聚合后得到的结果


    2. 行动操作
    行动操作是真正触发计算的地方。Spark程序执行到行动操作时,才会执行真正的计算,从文件中加载数据,完成一次又一次转换操作,最终,完成行动操作得到结果。

    count() 返回数据集中的元素个数
    collect() 以数组的形式返回数据集中的所有元素
    first() 返回数据集中的第一个元素
    take(n) 以数组的形式返回数据集中的前n个元素
    reduce(func) 通过函数func(输入两个参数并返回一个值)聚合数据集中的元素
    foreach(func) 将数据集中的每个元素传递到函数func中运行
    scala> val rdd=sc.parallelize(Array(1,2,3,4,5))
    rdd: org.apache.spark.rdd.RDD[Int]=ParallelCollectionRDD[1] at parallelize at <console>:24
    scala> rdd.count()
    res0: Long = 5
    res1: Int = 1
    scala> rdd.take(3)
    res2: Array[Int] = Array(1,2,3)
    scala> rdd.reduce((a,b)=>a+b)
    res3: Int = 15
    scala> rdd.collect()
    res4: Array[Int] = Array(1,2,3,4,5)
    scala> rdd.foreach(elem=>println(elem))
    1
    2
    3
    4
    5

    3. 惰性机制
    所谓的“惰性机制”是指,整个转换过程只是记录了转换的轨迹,并不会发生真正的计算,只有遇到行动操作时,才会触发“从头到尾”的真正的计算。这里给出一段简单的语句来解释Spark的惰性机制。

    scala> val lines = sc.textFile("data.txt")
    scala> val lineLengths = lines.map(s => s.length)
    scala> val totalLength = lineLengths.reduce((a, b) => a + b)

    1.3 持久化
    在Spark中,RDD采用惰性求值的机制,每次遇到行动操作,都会从头开始执行计算。每次调用行动操作,都会触发一次从头开始的计算。这对于迭代计算而言,代价是很大的,迭代计算经常需要多次重复使用同一组数据

    scala> val list = List("Hadoop","Spark","Hive")
    list: List[String] = List(Hadoop, Spark, Hive)
    scala> val rdd = sc.parallelize(list)
    rdd: org.apache.spark.rdd.RDD[String] = ParallelCollectionRDD[22] at parallelize at <console>:29
    scala> println(rdd.count()) //行动操作,触发一次真正从头到尾的计算
    3
    scala> println(rdd.collect().mkString(",")) //行动操作,触发一次真正从头到尾的计算
    Hadoop,Spark,Hive

    可以通过持久化(缓存)机制避免这种重复计算的开销
    可以使用persist()方法对一个RDD标记为持久化
    之所以说“标记为持久化”,是因为出现persist()语句的地方,并不会马上计算生成RDD并把它持久化,而是要等到遇到第一个行动操作触发真正计算以后,才会把计算结果进行持久化
    持久化后的RDD将会被保留在计算节点的内存中被后面的行动操作重复使用
    persist()的圆括号中包含的是持久化级别参数:
    persist(MEMORY_ONLY):表示将RDD作为反序列化的对象存储于JVM中,如果内存不足,就要按照LRU原则替换缓存中的内容
    persist(MEMORY_AND_DISK)表示将RDD作为反序列化的对象存储在JVM中,如果内存不足,超出的分区将会被存放在硬盘上
    一般而言,使用cache()方法时,会调用persist(MEMORY_ONLY)
    可以使用unpersist()方法手动地把持久化的RDD从缓存中移除
    针对上面的实例,增加持久化语句以后的执行过程如下:

    scala> val list = List("Hadoop","Spark","Hive")
    list: List[String] = List(Hadoop, Spark, Hive)
    scala> val rdd = sc.parallelize(list)
    rdd: org.apache.spark.rdd.RDD[String] = ParallelCollectionRDD[22] at parallelize at <console>:29
    scala> rdd.cache() //会调用persist(MEMORY_ONLY),但是,语句执行到这里,并不会缓存rdd,因为这时rdd还没有被计算生成
    scala> println(rdd.count()) //第一次行动操作,触发一次真正从头到尾的计算,这时上面的rdd.cache()才会被执行,把这个rdd放到缓存中
    3
    scala> println(rdd.collect().mkString(",")) //第二次行动操作,不需要触发从头到尾的计算,只需要重复使用上面缓存中的rdd
    Hadoop,Spark,Hive

    1.4 分区
    RDD是弹性分布式数据集,通常RDD很大,会被分成很多个分区,分别保存在不同的节点上
    1.分区的作用
    (1)增加并行度


    (2)减少通信开销


    2.RDD分区原则
    RDD分区的一个原则是使得分区的个数尽量等于集群中的CPU核心(core)数目
    对于不同的Spark部署模式而言(本地模式、Standalone模式、YARN模式、Mesos模式),都可以通过设置spark.default.parallelism这个参数的值,来配置默认的分区数目,一般而言:
    *本地模式:默认为本地机器的CPU数目,若设置了local[N],则默认为N
    *Apache Mesos:默认的分区数为8
    *Standalone或YARN:在“集群中所有CPU核心数目总和”和“2”二者中取较大值作为默认值
    3.设置分区的个数
    (1)创建RDD时手动指定分区个数
    在调用textFile()和parallelize()方法的时候手动指定分区个数即可,语法格式如下:
    sc.textFile(path, partitionNum)
    其中,path参数用于指定要加载的文件的地址,partitionNum参数用于指定分区个数。

    scala> val array = Array(1,2,3,4,5)
    scala> val rdd = sc.parallelize(array,2) //设置两个分区

    (2)使用reparititon方法重新设置分区个数
    通过转换操作得到新 RDD 时,直接调用 repartition 方法即可。例如:

    scala> val data = sc.textFile("file:///usr/local/spark/mycode/rdd/word.txt",2)
    data: org.apache.spark.rdd.RDD[String] = file:///usr/local/spark/mycode/rdd/word.txt MapPartitionsRDD[12] at textFile at <console>:24
    scala> data.partitions.size //显示data这个RDD的分区数量
    res2: Int=2
    scala> val rdd = data.repartition(1) //对data这个RDD进行重新分区
    rdd: org.apache.spark.rdd.RDD[String] = MapPartitionsRDD[11] at repartition at :26
    scala> rdd.partitions.size
    res4: Int = 1

    4.自定义分区方法
    Spark提供了自带的HashPartitioner(哈希分区)与RangePartitioner(区域分区),能够满足大多数应用场景的需求。与此同时,Spark也支持自定义分区方式,即通过提供一个自定义的Partitioner对象来控制RDD的分区方式,从而利用领域知识进一步减少通信开销。
    要实现自定义分区,需要定义一个类,这个自定义类需要继承org.apache.spark.Partitioner类,并实现下面三个方法:
      numPartitions: Int 返回创建出来的分区数
      getPartition(key: Any): Int 返回给定键的分区编号(0到numPartitions-1)
      equals() Java判断相等性的标准方法

    2 键值对RDD
    2.1 键值对RDD的创建
    (1)第一种创建方式:从文件中加载
    可以采用多种方式创建Pair RDD,其中一种主要方式是使用map()函数来实现

    scala> val lines = sc.textFile("file:///usr/local/spark/mycode/pairrdd/word.txt")
    lines: org.apache.spark.rdd.RDD[String] = file:///usr/local/spark/mycode/pairrdd/word.txt MapPartitionsRDD[1] at textFile at <console>:27
    scala> val pairRDD = lines.flatMap(line => line.split(" ")).map(word => (word,1))
    pairRDD: org.apache.spark.rdd.RDD[(String, Int)] = MapPartitionsRDD[3] at map at <console>:29
    scala> pairRDD.foreach(println)
    (i,1)
    (love,1)
    (hadoop,1)
    ……

    (2)第二种创建方式:通过并行集合(数组)创建RDD

    scala> val list = List("Hadoop","Spark","Hive","Spark")
    list: List[String] = List(Hadoop, Spark, Hive, Spark)
     
    scala> val rdd = sc.parallelize(list)
    rdd: org.apache.spark.rdd.RDD[String] = ParallelCollectionRDD[11] at parallelize at <console>:29
     
    scala> val pairRDD = rdd.map(word => (word,1))
    pairRDD: org.apache.spark.rdd.RDD[(String, Int)] = MapPartitionsRDD[12] at map at <console>:31
     
    scala> pairRDD.foreach(println)
    (Hadoop,1)
    (Spark,1)
    (Hive,1)	
    (Spark,1)

    2.2 常用的键值对RDD转换操作
    reduceByKey(func) reduceByKey(func)的功能是,使用func函数合并具有相同键的值

    (Hadoop,1)
    (Spark,1)
    (Hive,1)
    (Spark,1)
    scala> pairRDD.reduceByKey((a,b)=>a+b).foreach(println)
    (Spark,2)
    (Hive,1)
    (Hadoop,1)

    groupByKey() groupByKey()的功能是,对具有相同键的值进行分组
    比如,对四个键值对("spark",1)、("spark",2)、("hadoop",3)和("hadoop",5),采用groupByKey()后得到的结果是:("spark",(1,2))和("hadoop",(3,5))

    scala> pairRDD.groupByKey()
    res15: org.apache.spark.rdd.RDD[(String, Iterable[Int])] = ShuffledRDD[15] at groupByKey at <console>:34

    reduceByKey和groupByKey的区别:
    reduceByKey用于对每个key对应的多个value进行merge操作,最重要的是它能够在本地先进行merge操作,并且merge操作可以通过函数自定义
    groupByKey也是对每个key进行操作,但只生成一个sequence,groupByKey本身不能自定义函数,需要先用groupByKey生成RDD,然后才能对此RDD通过map进行自定义函数操作

    scala> val words = Array("one", "two", "two", "three", "three", "three")  
    scala> val wordPairsRDD = sc.parallelize(words).map(word => (word, 1))  
    scala> val wordCountsWithReduce = wordPairsRDD.reduceByKey(_ + _)    
    scala> val wordCountsWithGroup = wordPairsRDD.groupByKey().map(t => (t._1, t._2.sum)) 

    上面得到的wordCountsWithReduce和wordCountsWithGroup是完全一样的,但是,它们的内部运算过程是不同的
    (1)当采用reduceByKey时,Spark可以在每个分区移动数据之前将待输出数据与一个共用的key结合


    (2)当采用groupByKey时,由于它不接收函数,Spark只能先将所有的键值对(key-value pair)都移动,这样的后果是集群节点之间的开销很大,导致传输延时

    keys keys只会把Pair RDD中的key返回形成一个新的RDD

    (Hadoop,1)
    (Spark,1)
    (Hive,1)
    (Spark,1)
    
    scala> pairRDD.keys
    res17: org.apache.spark.rdd.RDD[String] = MapPartitionsRDD[17] at keys at <console>:34
    scala> pairRDD.keys.foreach(println)
    Hadoop
    Spark
    Hive
    Spark

    values values只会把Pair RDD中的value返回形成一个新的RDD。

    scala> pairRDD.values
    res0: org.apache.spark.rdd.RDD[Int] = MapPartitionsRDD[2] at values at <console>:34 
    scala> pairRDD.values.foreach(println)
    1
    1
    1
    1

    sortByKey() sortByKey()的功能是返回一个根据键排序的RDD

    scala> pairRDD.sortByKey()
    res0: org.apache.spark.rdd.RDD[(String, Int)] = ShuffledRDD[2] at sortByKey at <console>:34
    scala> pairRDD.sortByKey().foreach(println)
    (Hadoop,1)
    (Hive,1)
    (Spark,1)
    (Spark,1)

    sortByKey()和sortBy()

    scala> val d1 = sc.parallelize(Array((“c",8),(“b“,25),(“c“,17),(“a“,42),(“b“,4),(“d“,9),(“e“,17),(“c“,2),(“f“,29),(“g“,21),(“b“,9))) 
    scala> d1.reduceByKey(_+_).sortByKey(false).collect
    res2: Array[(String, Int)] = Array((g,21),(f,29),(e,17),(d,9),(c,27),(b,38),(a,42))
    
    scala> val d2 = sc.parallelize(Array((“c",8),(“b“,25),(“c“,17),(“a“,42),(“b“,4),(“d“,9),(“e“,17),(“c“,2),(“f“,29),(“g“,21),(“b“,9))) 
    scala> d2.reduceByKey(_+_).sortBy(_._2,false).collect
    res4: Array[(String, Int)] = Array((a,42),(b,38),(f,29),(c,27),(g,21),(e,17),(d,9))

    mapValues(func) 对键值对RDD中的每个value都应用一个函数,但是,key不会发生变化

    scala> pairRDD.mapValues(x => x+1)
    res2: org.apache.spark.rdd.RDD[(String, Int)] = MapPartitionsRDD[4] at mapValues at <console>:34
    scala> pairRDD.mapValues(x => x+1).foreach(println)
    (Hadoop,2)
    (Spark,2)
    (Hive,2)
    (Spark,2)

    join join就表示内连接。对于内连接,对于给定的两个输入数据集(K,V1)和(K,V2),只有在两个数据集中都存在的key才会被输出,最终得到一个(K,(V1,V2))类型的数据集。

    scala> val pairRDD1 = sc.parallelize(Array(("spark",1),("spark",2),("hadoop",3),("hadoop",5)))
    pairRDD1: org.apache.spark.rdd.RDD[(String, Int)] = ParallelCollectionRDD[24] at parallelize at <console>:27
     
    scala> val pairRDD2 = sc.parallelize(Array(("spark","fast")))
    pairRDD2: org.apache.spark.rdd.RDD[(String, String)] = ParallelCollectionRDD[25] at parallelize at <console>:27
     
    scala> pairRDD1.join(pairRDD2)
    res9: org.apache.spark.rdd.RDD[(String, (Int, String))] = MapPartitionsRDD[28] at join at <console>:32
     
    scala> pairRDD1.join(pairRDD2).foreach(println)
    (spark,(1,fast))
    (spark,(2,fast))
    

    combineByKey 

    combineByKey(createCombiner,mergeValue,mergeCombiners,partitioner,mapSideCombine)
    createCombiner:在第一次遇到Key时创建组合器函数,将RDD数据集中的V类型值转换C类型值(V => C)
    mergeValue:合并值函数,再次遇到相同的Key时,将createCombiner的C类型值与这次传入的V类型值合并成一个C类型值(C,V)=>C
    mergeCombiners:合并组合器函数,将C类型值两两合并成一个C类型值
    partitioner:使用已有的或自定义的分区函数,默认是HashPartitioner 
    mapSideCombine:是否在map端进行Combine操作,默认为true

    3 数据读写
    1.本地文件系统的数据读写
    (1)从文件中读取数据创建RDD

    scala> val textFile = sc.textFile("file:///usr/local/spark/mycode/wordcount/word.txt")

    因为Spark采用了惰性机制,在执行转换操作的时候,即使输入了错误的语句,spark-shell也不会马上报错(假设word.txt不存在)
    (2)把RDD写入到文本文件中

    scala> val textFile = sc.textFile("file:///usr/local/spark/mycode/wordcount/word.txt")
    scala> textFile.saveAsTextFile("file:///usr/local/spark/mycode/wordcount/writeback")

    如果想再次把数据加载在RDD中,只要使用writeback这个目录即可,如下:

    scala> val textFile = sc.textFile("file:///usr/local/spark/mycode/wordcount/writeback")

    2.分布式文件系统HDFS的数据读写
    从分布式文件系统HDFS中读取数据,也是采用textFile()方法,可以为textFile()方法提供一个HDFS文件或目录地址,如果是一个文件地址,它会加载该文件,如果是一个目录地址,它会加载该目录下的所有文件的数据

    scala> val textFile = sc.textFile("hdfs://localhost:9000/user/hadoop/word.txt")
    scala> textFile.first()

    如下三条语句都是等价的:

    scala> val textFile = sc.textFile("hdfs://localhost:9000/user/hadoop/word.txt")
    scala> val textFile = sc.textFile("/user/hadoop/word.txt")
    scala> val textFile = sc.textFile("word.txt")

    同样,可以使用saveAsTextFile()方法把RDD中的数据保存到HDFS文件中,命令如下:

    scala> textFile.saveAsTextFile("writeback")

    3.JSON文件的数据读写
    JSON(JavaScript Object Notation) 是一种轻量级的数据交换格式
    Spark提供了一个JSON样例数据文件,存放在“/usr/local/spark/examples/src/main/resources/people.json”中
      {"name":"Michael"}
      {"name":"Andy", "age":30}
      {"name":"Justin", "age":19}
    把本地文件系统中的people.json文件加载到RDD中:

    scala> val jsonStr = sc.textFile("file:///usr/local/spark/examples/src/main/resources/people.json")
    scala> jsonStr.foreach(println)
    {"name":"Michael"}
    {"name":"Andy", "age":30}
    {"name":"Justin", "age":19}

    4.读写HBase数据
    1. 创建一个HBase表

    2. 配置Spark

    3. 编写程序读取HBase数据
    如果要让Spark读取HBase,就需要使用SparkContext提供的newAPIHadoopRDD这个API将表的内容以RDD的形式加载到Spark中。

    import org.apache.hadoop.conf.Configuration
    import org.apache.hadoop.hbase._
    import org.apache.hadoop.hbase.client._
    import org.apache.hadoop.hbase.mapreduce.TableInputFormat
    import org.apache.hadoop.hbase.util.Bytes
    import org.apache.spark.SparkContext
    import org.apache.spark.SparkContext._
    import org.apache.spark.SparkConf
    object SparkOperateHBase {
    def main(args: Array[String]) {
    val conf = HBaseConfiguration.create()
    val sc = new SparkContext(new SparkConf())
    //设置查询的表名
    conf.set(TableInputFormat.INPUT_TABLE, "student")
    val stuRDD = sc.newAPIHadoopRDD(conf, classOf[TableInputFormat],
    classOf[org.apache.hadoop.hbase.io.ImmutableBytesWritable],
    classOf[org.apache.hadoop.hbase.client.Result])
    val count = stuRDD.count()
    println("Students RDD Count:" + count)
    stuRDD.cache()
    //遍历输出
    stuRDD.foreach({ case (_,result) =>
    val key = Bytes.toString(result.getRow)
    val name = Bytes.toString(result.getValue("info".getBytes,"name".getBytes))
    val gender = Bytes.toString(result.getValue("info".getBytes,"gender".getBytes))
    val age = Bytes.toString(result.getValue("info".getBytes,"age".getBytes))
    println("Row key:"+key+" Name:"+name+" Gender:"+gender+" Age:"+age)
    })
    }
    }

    4. 编写程序向HBase写入数据

    import org.apache.hadoop.hbase.HBaseConfiguration 
    import org.apache.hadoop.hbase.mapreduce.TableOutputFormat 
    import org.apache.spark._ 
    import org.apache.hadoop.mapreduce.Job 
    import org.apache.hadoop.hbase.io.ImmutableBytesWritable 
    import org.apache.hadoop.hbase.client.Result 
    import org.apache.hadoop.hbase.client.Put 
    import org.apache.hadoop.hbase.util.Bytes
    object SparkWriteHBase { 
    def main(args: Array[String]): Unit = { 
    val sparkConf = new SparkConf().setAppName("SparkWriteHBase").setMaster("local") 
    val sc = new SparkContext(sparkConf) 
    val tablename = "student" 
    sc.hadoopConfiguration.set(TableOutputFormat.OUTPUT_TABLE, tablename) 
    val job = new Job(sc.hadoopConfiguration) 
    job.setOutputKeyClass(classOf[ImmutableBytesWritable]) 
    job.setOutputValueClass(classOf[Result]) 
    job.setOutputFormatClass(classOf[TableOutputFormat[ImmutableBytesWritable]]) 
    val indataRDD = sc.makeRDD(Array("3,Rongcheng,M,26","4,Guanhua,M,27")) //构建两行记录
    val rdd = indataRDD.map(_.split(',')).map{arr=>{ 
    val put = new Put(Bytes.toBytes(arr(0))) //行健的值 
    put.add(Bytes.toBytes("info"),Bytes.toBytes("name"),Bytes.toBytes(arr(1))) //info:name列的值
    put.add(Bytes.toBytes("info"),Bytes.toBytes("gender"),Bytes.toBytes(arr(2))) //info:gender列的值
    put.add(Bytes.toBytes("info"),Bytes.toBytes("age"),Bytes.toBytes(arr(3).toInt)) //info:age列的值
    (new ImmutableBytesWritable, put) 
    }} 
    rdd.saveAsNewAPIHadoopDataset(job.getConfiguration()) 
    } 
    }

    学习参考:林子雨大数据原理与应用课件,Chapter5-厦门大学-林子雨-Spark编程基础-第5章-RDD编程(2018年2月).ppt

    图片来源:Chapter5-厦门大学-林子雨-Spark编程基础-第5章-RDD编程(2018年2月).ppt

  • 相关阅读:
    fullCalendar改造计划之带农历节气节假日的万年历(转)
    Linked List Cycle
    Remove Nth Node From End of List
    Binary Tree Inorder Traversal
    Unique Binary Search Trees
    Binary Tree Level Order Traversal
    Binary Tree Level Order Traversal II
    Plus One
    Remove Duplicates from Sorted List
    Merge Two Sorted Lists
  • 原文地址:https://www.cnblogs.com/flw0322/p/12269251.html
Copyright © 2011-2022 走看看