编程编程练习网站

上周,我们第一次体验了编程风格的练习。 请记住,目标是编写一个简单的程序,但要遵守一些约束。 先前的限制是只有一个变量可用,即数组。 对于像Kotlin这样的静态类型的语言,在使用它们之前,需要将大量的变量强制转换为正确的类型。

本周,约束条件是极端的,但有所不同。 除了两个数组,我们还有两个可用的数据结构:

  • 哈希图-也称为字典或关联数组-称为
  • 一个恰当地命名为...的堆栈

目标是在堆栈上执行所有操作,并且仅在必要时在堆上存储数据。 幸运的是,我曾经参加过关于非主流语言的主题演讲,包括基于堆栈的语言PostScript。 因此,我对基于堆栈的语言如何工作有些熟悉。 但是,如果您对它们一无所知,我相信这篇文章将会很有趣。

这是第2 后,在编程风格焦点series.Other职位的练习包括:

  1. 以编程风格介绍练习
  2. 以编程风格进行练习,将内容堆叠起来 (本文)
  3. 编程风格的练习,Kwisatz Haderach风格
  4. 编程风格的练习,递归
  5. 具有高阶功能的编程风格的练习
  6. 以编程风格进行练习
  7. 以编程风格进行练习,回到面向对象的编程
  8. 编程风格的练习:地图也是对象
  9. 编程风格的练习:事件驱动的编程
  10. 编程风格的练习和事件总线
  11. 反思编程风格的练习
  12. 面向方面的编程风格的练习
  13. 编程风格的练习:FP&I / O
  14. 关系数据库风格的练习
  15. 编程风格的练习:电子表格
  16. 并发编程风格的练习
  17. 编程风格的练习:在线程之间共享数据
  18. 使用Hazelcast以编程风格进行练习
  19. MapReduce风格的练习
  20. 编程风格的练习总结

堆栈和基于堆栈的语言快速入门

我假设您对称为Stack的数据结构有些熟悉。 这是一个专门的队列,具有先进先出,语义先进的语义。 类似于一堆板:假设将板A放在堆栈上,然后将板B再放在板C上。一个人只能从堆栈中将板放在顶部:因此,顺序将是C,然后是B,然后是一个。

同样,基于堆栈的语言将变量放在堆栈上。 要调用一个函数,首先需要将参数压入堆栈,然后才调用函数。 然后,一个函数将从堆栈中弹出其参数,并将结果压入堆栈。

一个简单的加法将转换为以下伪代码:

PUT 1
PUT 2
ADD

这样,函数ADD将弹出2 ,然后弹出1 ,将它们求和并将结果压入堆栈。

在Kotlin中,这可能会翻译为以下内容:

stack.push(1)
stack.push(2)
stack.push(
    (stack.pop()asInt)+(stack.pop()asInt)
)

可以创建一个add()函数:

funadd()=stack.push(
    (stack.pop()asInt)+(stack.pop()asInt)
)

stack.push(1)
stack.push(2)
add()

练习中的堆栈

程序的开头是一个很好的示例,可以理解如何超越上述简单示例:

funrun(filename:String):Map<*,*>{
    stack.push(filename) (1)
    readFile()
    filterChars()
    ...
}

funreadFile(){
    valf=read(stack.pop()asString) (2)
    stack.push(f) (3)
}
  1. 将作为参数传递的文件名压入堆栈
  2. 弹出文件名并读取文件内容
  3. 将文件行推入堆栈

read()函数将文件名作为参数,因为它是所有章节之间共享的共享实用程序函数。 如果要完全遵守约束,则应将其更改为不接受任何参数,并从堆栈中获取文件名。 其他功能不带参数,并严格遵守规则。

funfilterChars(){
    stack.push( (1)
        (stack.pop()asList<*>) (2)
            .map{"\\W|_".toRegex().replace(itasString," ")} (3)
            .map(String::toLowerCase)) (3)
}
  1. 将处理结果压入堆栈
  2. 弹出文本行作为List进行处理
  3. 处理字符串

现在,此功能几乎完全符合锻炼的精神。

为下一步做准备

实际上,以前的代码在我看来有点作弊: map()是Kotlin的stdlib中的函数,应在堆栈上实现。

这需要两个可以通过Kotlin扩展功能实现的帮助程序功能:

  1. 一个extend()方法(类似于Python),它获取一个集合,并将其所有元素压入堆栈:
    fun<T>Stack<T>.extend(iterable:Iterable<T>){
        iterable.forEach{push(it)}
    }
  2. 一个swap()方法,用于交换堆栈中前两个元素的位置:
    funStack<Any>.swap(){
        vala=pop() (1)
        valb=pop() (1)
        push(a)
        push(b)
    }
    1. 通常应禁止使用局部变量,而应使用堆。 但是,我允许自己使用这个小捷径,因为它看起来更好,并且变化不大。

整个九码

最简单的部分是通过用extend()替换push()来更新readFile()函数的实现:

funreadFile(){
    valf=read(stack.pop()asString)
    stack.extend(f) (1)
}
  1. 此时,堆栈应在读取文件中每行文本包含一个元素

下一步需要从堆栈中弹出每个字符串,对其进行处理,然后将其推回。 但是,由于堆栈的性质,处理后的字符串将放回顶部。 并且只能访问顶部字符串。 我们可以使用堆而不是将字符串推回堆栈上,但这会作弊...

完全符合堆栈要求的替代方法如下:

  1. 将可变列表压入堆栈
  2. 重复以下步骤,直到堆栈上只有一个元素
    1. 交换前两个元素-列表和字符串
    2. 弹出第一个元素-这将是要处理的字符串
    3. 处理字符串
    4. 弹出现在的第一个元素-这将是列表
    5. 将处理后的字符串添加到列表中
    6. 将列表推回堆栈
  3. 最后,弹出包含所有已处理字符串的列表
  4. 并将其内容作为单独的字符串压入堆栈

还有另外一招。 要将元素添加到可变列表,API为list.add(string) 。 在堆栈上,这将转换为stack.pop().add(stack.pop()) 。 这意味着列表应该是堆栈中的第一项。 但是,我们过程的顺序相反:第一个项目应该是字符串,第二个应该是列表。 因此,我们需要一个函数来反转调用者(列表)和被调用者(字符串)的顺序。 这可以通过另一个扩展功能轻松完成:

fun<T>T.addTo(collection:MutableCollection<T>)=collection.also{it.add(this)}

相关的实现是:

funfilterChars(){
    stack.push(mutableListOf<String>())
    while(stack.size>1){
        stack.swap()
        stack.push(
            "\\W|_".toRegex()
                .replace((stack.pop()asString)," ")
                .toLowerCase()
                .addTo(stack.pop()asMutableList<Any>)
        )
    }
    stack.extend(stack.pop()asMutableList<Any>)
}

将一个集合压入堆栈,交换,迭代弹出直到堆栈上只剩下一个元素,然后将结果压入堆栈的技巧,因为单个项目可以在其他函数中复制。 它仅需要其他扩展功能,具体取决于集合的类型, 例如 MutableMap

其他注意事项

还有一些其他注意事项值得一提。

评价

评估堆栈中一项的第一种方法是将其弹出,将其存储在堆中,然后在必要时将其再次推回。 可以通过使用peek()方法来避免这种情况: peek()允许获取对堆栈中第一个项目的引用,但不会从堆栈中弹出它-它仍然存在。

if((stack.peek()asString).length<2){
    // Do something
}

过滤

评估用于过滤项目。 要从堆栈中丢弃物品,只需将其弹出...就可以了。

if((stack.peek()asString).length<2)stack.pop() (1)
else(heap["words"]asMutableList<Any>).add(stack.pop()) (2)
  1. 筛选出项目
  2. 将项目存储在堆上

自定义堆栈实现

本章的另一项任务是创建自己的堆栈实现。 JDK中提供的默认Stack类存在一些问题:

  • 它继承自AbsractList ,从而实现List 这意味着,尽管它提供了特定于堆栈的方法,例如pop()push() ,但它也从整个继承层次结构中泄漏了许多无关的方法,例如add()remove() 尽管我们的代码没有使用那些不相关的方法,但这仍然是一个糟糕的设计决策。
  • 它的父类是Vector 虽然每本身不会被弃用,这类下跌的青睐,因为在其使用的synchronized关键字。 从纯粹的性能角度来看,最好使用标准的ArrayList 如果需要线程安全,则可以在其周围使用Collections.synchronizedList()

由于设计和性能这两个原因,很少使用默认的Stack实现。

从设计的角度来看,自定义堆栈的合同非常简单:

interfaceStack<T>{
    valsize:Int
    funpush(t:T?)
    funpop():T
    funpeek():T
    funisNotEmpty()
}

从性能的角度看,看起来链表实现就足够了。 有没有搜索,仅添加/移除的第一个元素:在链接列表中的搜索是O(n),而添加和删除所述第一元件只O(1)。 JDK API提供了一个链接列表的实现,恰当地命名为LinkedList 。 使用它作为我们自定义堆栈合同中的委托是一件容易的事:

classStack<T>{

    privatevallist=LinkedList<T>()

    valsize:Int
        get()=list.size

    funpush(t:T?)=list.addFirst(t)
    funpop():T=list.removeFirst()
    funpeek():T=list.peekFirst()
    funisNotEmpty()=list.isNotEmpty()
}

结论

与上周的单个数组约束相比,代码库中需要的强制转换更少。

另一方面,堆栈机制要花一些时间来习惯,特别是如果要避免使用堆时:相关的心理健美操很有趣,尤其是交换。

这篇文章的完整源代码可以在Github上找到。

翻译自: https://blog.frankel.ch/exercises-programming-style/2/

编程编程练习网站

Logo

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐