【问题标题】:Scala - get unique values from List with a twistScala - 从 List 中获取独特的值
【发布时间】:2011-05-25 17:17:28
【问题描述】:

我有一个这样的列表:

val l= List(("Agent", "PASS"), ("Agent", "FAIL"), ("Agent 1", "FAIL"), ("Agent", "PASS"), ("Agent 2", "PASS") )

我最终需要一个这样的列表:

val filteredList= List(("Agent", "FAIL"), ("Agent 1", "FAIL"), ("Agent 2", "PASS") )

发生了什么?

("Agent", "PASS"), ("Agent", "FAIL")

变成

("Agent", "FAIL")

(因为如果至少有一个 FAIL,我需要保留该条目)

代理 1 和代理 2 的条目保持不变,因为每个条目只有一个。

我找到的最接近的答案是 How in Scala to find unique items in List 但我不知道如何保留 FAIL 的条目。

我希望问题清楚,如果没有,我可以给你一个更好的例子。

谢谢

【问题讨论】:

    标签: scala scala-collections


    【解决方案1】:

    序言

    我想到状态可以被视为具有优先级,如果给定一系列(agent,status) 对,那么任务就是只为每个代理选择最高优先级的状态。不幸的是,status 不是强类型的,并没有使用如此定义的显式排序,但是......因为它是一个只有两个值的字符串,我们可以安全地使用字符串排序,因为它与优先级具有 1:1 的对应关系。

    我的两个答案都利用了两个有用的事实:

    1. 在自然字符串排序中,"FAIL" < "PASS",所以:

      List("PASS", "FAIL", "PASS").sorted.head = "FAIL"
      
    2. 对于两个元组(x,a)(x,b)(x,a) > (x, b)如果(a > b)

    更新回复

    val solution = l.sorted.reverse.toMap
    

    当通过.toMap 方法将Seq[(A,B)] 转换为Map[A,B] 时,原始元组序列中的每个“键”只能在生成的Map 中出现一次。碰巧的是,转换使用最后一次发生的此类事件。

    l.sorted.reverse = List(
      (Agent 2,PASS),  // <-- Last "Agent 2"
      (Agent 1,FAIL),  // <-- Last "Agent 1"
      (Agent,PASS),
      (Agent,PASS),
      (Agent,FAIL))    // <-- Last "Agent"
    
    l.sorted.reverse.toMap = Map(
      Agent 2 -> PASS,
      Agent 1 -> FAIL,
      Agent -> FAIL)
    

    原始回复

    从答案开始……

    val oldSolution = (l groupBy (_._1)) mapValues {_.sorted.head._2}
    

    ...然后展示我的工作:)

    //group
    l groupBy (_._1) = Map(
      Agent 2 -> List((Agent 2,PASS)),
      Agent 1 -> List((Agent 1,FAIL)),
      Agent -> List((Agent,PASS), (Agent,FAIL), (Agent,PASS))
    )
    
    //extract values
    (l groupBy (_._1)) mapValues {_.map(_._2)} = Map(
      Agent 2 -> List(PASS),
      Agent 1 -> List(FAIL),
      Agent -> List(PASS, FAIL, PASS))
    
    //sort
    (l groupBy (_._1)) mapValues {_.map(_._2).sorted} = Map(
      Agent 2 -> List(PASS),
      Agent 1 -> List(FAIL),
      Agent -> List(FAIL, PASS, PASS))
    
    //head
    (l groupBy (_._1)) mapValues {_.map(_._2).sorted.head} = Map(
      Agent 2 -> PASS,
      Agent 1 -> FAIL,
      Agent -> FAIL)
    

    但是,您可以直接对agent -&gt; status 对进行排序,而无需先提取_2

    //group & sort
    (l groupBy (_._1)) mapValues {_.sorted} = Map(
      Agent 2 -> List((Agent 2,PASS)),
      Agent 1 -> List((Agent 1,FAIL)),
      Agent -> List((Agent,FAIL), (Agent,PASS), (Agent,PASS)))
    
    //extract values
    (l groupBy (_._1)) mapValues {_.sorted.head._2} = Map(
      Agent 2 -> PASS,
      Agent 1 -> FAIL,
      Agent -> FAIL)
    

    在任何一种情况下,如果您愿意,请随时转换回对列表:

    l.sorted.reverse.toMap.toList = List(
      (Agent 2, PASS),
      (Agent 1, FAIL),
      (Agent, FAIL))
    

    【讨论】:

    • l.sorted.reverse.toMap 非常漂亮,尽管我敢肯定我永远不会记得为什么它在未来会起作用。 ;-)
    • 排序是 O(nlogn) (最好的情况),而这个问题显然可以在不超过 O(n) 的时间内解决。只使用maxmin 不是更好吗,它们也取决于排序而不是排序?
    • @Daniel 哦,绝对是如果列表足够大以至于这会影响性能。我无法抗拒用四个令牌解决它的优雅:)
    【解决方案2】:

    这是你想要的吗?

    jem@Respect:~$ scala
    Welcome to Scala version 2.8.0.final (Java HotSpot(TM) Client VM, Java 1.6.0_21).
    Type in expressions to have them evaluated.
    Type :help for more information.
    
    scala> val l= List(("Agent", "PASS"), ("Agent", "FAIL"), ("Agent 1", "FAIL"), ("Agent", "PASS"), ("Agent 2", "PASS") )
    l: List[(java.lang.String, java.lang.String)] = List((Agent,PASS), (Agent,FAIL), (Agent 1,FAIL), (Agent,PASS), (Agent 2,PASS))
    
    scala> l.foldLeft(Map.empty[String, String]){(map,next) =>
         |   val (agent, result) = next
         |   if ("FAIL" == result) map.updated(agent, result)
         |   else {           
         |     val maybeExistingResult = map.get(agent)
         |     if (maybeExistingResult.map(_ == "FAIL").getOrElse(false)) map
         |     else map.updated(agent, result)
         |   }
         | }
    res0: scala.collection.immutable.Map[String,String] = Map((Agent,FAIL), (Agent 1,FAIL), (Agent 2,PASS))
    
    scala> res0.toList
    res1: List[(String, String)] = List((Agent 2,PASS), (Agent 1,FAIL), (Agent,FAIL))
    

    或者这里有一个更短更晦涩的解决方案:

    scala> l.groupBy(_._1).map(pair => (pair._1, pair._2.reduceLeft((a,b) => if ("FAIL" == a._2 || "FAIL" == b._2) (a._1, "FAIL") else a))).map(_._2).toList
    res2: List[(java.lang.String, java.lang.String)] = List((Agent 2,PASS), (Agent 1,FAIL), (Agent,FAIL))
    

    【讨论】:

    • 哇...在一个罕见的转折中,它实际上比等效的命令式 Java 代码更冗长且更难理解。
    • 假设第二个字符串值被限制为“PASS”或“FAIL”,那么类型应该是布尔值。这将大大简化解决方案。
    • 如果有任何其他状态的可能性,那么状态字段应该是一个密封的特征或枚举。如果做不到这一点,我 100% 同意布尔值是最好的方法。
    【解决方案3】:

    有很多好的解决方案,但无论如何这里都是我的。 :-)

    l
    .groupBy(_._1) // group by key
    .map { 
        case (key, list) => 
            if (list.exists(_._2 == "FAIL")) (key, "FAIL") 
            else (key, "PASS")
    }
    

    这是我突然顿悟的另一个:

    def booleanToString(b: Boolean) = if (b) "PASS" else "FAIL"
    l
    .groupBy(_._1)
    .map {
        case (key, list) => key -> booleanToString(list.forall(_._2 == "PASS"))
    }
    

    【讨论】:

    • 这是迄今为止我见过的第二个最干净的解决方案,但可以通过使用 find 删除重复的“FAIL”:l groupBy (_._1) map {case (k,xs) =&gt; xs.find(_._2 == "FAIL").getOrElse(k-&gt;"PASS")} 使其更小一些。有趣的是,当我意识到“PASS”和“FAIL”都是字符串并且可以排序时,这就是导致我进入“eureka”时刻的思维链。
    • @Kevin 我不太喜欢排序解决方案,因为它是巧合,而不是因为 PASS 和 FAIl 的排序与代码应该做的事情有任何关系。
    • 在更严格的解决方案中,PASS 和 FAIL 将是密封特征的子类(例如 Status)或枚举的成员。在这种情况下,问题是过滤String -&gt; Status 的序列,以便为每个代理字符串找到最高优先级的状态,然后将按此优先级顺序显式定义状态对象的顺序。字符串表示“PASS”和“FAIL”碰巧也具有相同的顺序,这只是幸运的巧合。
    • 顺便说一句,如果有两个以上的字符串,我会明确定义一个专用函数在.sortBy() 中使用,而不是使用.sorted...解决方案仍然很优雅。
    【解决方案4】:

    这是我的看法。首先是一个功能解决方案:

    l.map(_._1).toSet.map({n:String=>(n, if(l contains (n,"FAIL")) "FAIL" else "PASS")})
    

    首先,我们唯一地隔离名称 (toSet),然后我们将每个名称映射到一个元组,将其自身作为第一个元素,如果 l 中包含失败,则将 "FAIL" 作为第二个元素,否则它显然必须是"PASS"

    结果是一个集合。当然,如果你真的需要一个列表,你可以在调用链的末尾做toList

    这是一个命令式的解决方案:

    var l = List(("Agent", "PASS"), ("Agent", "FAIL"), ("Agent 1", "FAIL"), ("Agent", "PASS"), ("Agent 2", "PASS"))
    l.foreach(t=>if(t._2=="FAIL") l=l.filterNot(_ == (t._1,"PASS")))
    l=l.toSet.toList
    

    我不太喜欢它,因为它是必要的,但是,嘿。从某种意义上说,它更好地反映了当您手动解决此问题时您实际上会做什么。对于您看到的每个"FAIL",您将删除所有对应的"PASS"es。之后,您确保唯一性 (.toSet.toList)。

    请注意,l 是命令式解决方案中的 var,这是必要的,因为它会被重新分配。

    【讨论】:

      【解决方案5】:

      Aggregate list values in Scala

      在您的情况下,您将按代理分组并通过折叠 PASS+PASS=>PASS 和 ANY+FAIL=>FAIL 进行聚合。

      【讨论】:

        【解决方案6】:

        或许先分组效率更高,然后求PASS/FAIL的析取:

        l.filter(_._2 == "PASS").toSet -- l.filter(_._2 == "FAIL").map(x => (x._1, "PASS"))
        

        这基于您的 ("Agent", "PASS") 输出,但如果您只想要代理:

        l.filter(_._2 == "PASS").map(x => x._1).toSet -- l.filter(_._2 == "FAIL").map(x => x._1)
        

        不知何故,我预计第二个会更短。

        【讨论】:

          【解决方案7】:

          据我了解,您希望:

          1. 按元组的第一个条目(“键”)对元组进行分组
          2. 对于每个键,检查所有元组第二个条目的值“FAIL”
          3. 如果您发现“FAIL”,则产生(key,“FAIL”),否则产生(key,“PASS”)

          由于我仍然觉得foldLeftreduceLeft 等难以阅读,这里将上述步骤直接翻译成理解:

          scala> for ((key, keyValues) <- l.groupBy{case (key, value) => key}) yield {
               |   val hasFail = keyValues.exists{case (key, value) => value == "FAIL"}
               |   (key, if (hasFail) "FAIL" else "PASS")                              
               | }
          res0: scala.collection.immutable.Map[java.lang.String,java.lang.String] = Map((Agent 2,PASS), (Agent 1,FAIL), (Agent,FAIL))
          

          如果您真的想要List,可以在最后拨打.toList

          编辑:稍作修改以使用exists 成语suggested by Daniel C. Sobral

          【讨论】:

            【解决方案8】:

            您需要保留原始订单吗?如果没有,我所知道的最短的解决方案(也很简单)是:

            {
              val fail = l.filter(_._2 == "FAIL").toMap        // Find all the fails
              l.filter(x => !fail.contains(x._1)) ::: fail.toList // All nonfails, plus the fails
            }
            

            但这不会删除额外的通行证。如果你想要,那么你需要一张额外的地图:

            {
              val fail = l.filter(_._2 == "FAIL").toMap
              l.toMap.filter(x => !fail.contains(x._1)).toList ::: fail.toList
            }
            

            另一方面,您可能希望按照最初找到它们的相同顺序获取元素。这比较棘手,因为您需要跟踪第一个有趣的项目出现的时间:

            {
              val fail = l.filter(_._2 == "FAIL").toMap
              val taken = new scala.collection.mutable.HashMap[String,String]
              val good = (List[Boolean]() /: l)((b,x) => {
                val okay = (!taken.contains(x._1) && (!fail.contains(x._1) || x._2=="FAIL"))
                if (okay) taken += x
                okay :: b
              }).reverse
              (l zip good).collect{ case (x,true) => x }
            }
            

            【讨论】:

              猜你喜欢
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 2021-06-20
              • 1970-01-01
              • 2018-03-06
              • 2019-05-12
              • 2018-03-30
              相关资源
              最近更新 更多