【问题标题】:How to write binary data to a file to implement Huffman compression如何将二进制数据写入文件以实现霍夫曼压缩
【发布时间】:2018-01-18 16:40:14
【问题描述】:

我正在尝试实现霍夫曼压缩。将 char 编码为 0 和 1 后,如何将其写入文件以便压缩?显然简单地写字符 0,1 只会使文件变大。

假设我有一个代表位的字符串“0101010”。 我希望将字符串写入文件,但与二进制一样,即不是字符 '0' 而是位 0。

我尝试在字符串上使用 getBytesArray(),但它似乎没有什么区别,而不是简单地写入字符串。

【问题讨论】:

  • 什么............你写的“位0”是什么意思。我想你有点误解了。
  • 写入二进制文件。
  • 您的示例字符串只有 7 个字符,所以只有 7 位。您不能以位写入文件,必须以字节为单位写入。那么你期望会发生什么?你真的总是有一个 8 个字符的乘积字符串吗?或者你有一些逻辑来填充最后一个字节(可能是0s 或1s)?还是别的什么?
  • 我正在尝试实现霍夫曼压缩。将 char 编码为 0 和 1 后,如何将其写入文件以便压缩?显然简单地写字符 0,1 只会使文件变大。
  • 只是为了明确这一点-您不能将少于一个字节的内容写入文件。这里有点描述如何使用“EOF”代码填充霍夫曼编码的字符串:www2.cs.duke.edu/csed/poop/huff/info

标签: scala binary huffman-code


【解决方案1】:

虽然 Sarvesh Kumar Singh 代码可能会起作用,但在我看来它是如此的 Javish,以至于我认为这个问题还需要一个答案。我想象在 Scala 中实现 Huffman 编码的方式是这样的:

import scala.collection._

type Bit = Boolean
type BitStream = Iterator[Bit]
type BitArray = Array[Bit]
type ByteStream = Iterator[Byte]
type CharStream = Iterator[Char]

case class EncodingMapping(charMapping: Map[Char, BitArray], eofCharMapping: BitArray)

def buildMapping(src: CharStream): EncodingMapping = {
  def accumulateStats(src: CharStream): Map[Char, Int] = ???

  def buildMappingImpl(stats: Map[Char, Int]): EncodingMapping = ???

  val stats = accumulateStats(src)
  buildMappingImpl(stats)
}

def convertBitsToBytes(bits: BitStream): ByteStream = {
  bits.grouped(8).map(bits => {
    val res = bits.foldLeft((0.toByte, 0))((acc, bit) => ((acc._1 * 2 + (if (bit) 1 else 0)).toByte, acc._2 + 1))
    // last byte might be less than 8 bits
    if (res._2 == 8)
      res._1
    else
      (res._1 << (8 - res._2)).toByte
  })
}

def encodeImpl(src: CharStream, mapping: EncodingMapping): ByteStream = {
  val mainData = src.flatMap(ch => mapping.charMapping(ch))
  val fullData = mainData ++ mapping.eofCharMapping
  convertBitsToBytes(fullData)
}

// can be used to encode String as src. Thanks to StringLike/StringOps extension
def encode(src: Iterable[Char]): (EncodingMapping, ByteStream) = {
  val mapping = buildMapping(src.iterator)
  val encoded = encode(src.iterator, mapping)
  (mapping, encoded)
}

def wrapClose[A <: java.io.Closeable, B](resource: A)(op: A => B): B = {
  try {
    op(resource)
  }
  finally {
    resource.close()
  }
}

def encodeFile(fileName: String): (EncodingMapping, ByteStream) = {
  // note in real life you probably want to specify file encoding as well
  val mapping = wrapClose(Source.fromFile(fileName))(buildMapping)
  val encoded = wrapClose(Source.fromFile(fileName))(file => encode(file, mapping))
  (mapping, encoded)
}

accumulateStats 中,您会发现每个CharsrcbuildMappingImpl(这是整个霍夫曼编码的主要部分)中出现的频率,您首先从@987654328 构建一棵树@ 然后使用创建一个固定的EncodingMappingeofCharMapping 是在其中一个 cmets 中提到的 pseudo-EOF char 的映射。请注意,高级 encode 方法会同时返回 EncodingMappingByteStream,因为在任何现实生活场景中,您都希望同时保存两者。

具体要求的逻辑位于convertBitsToBytes 方法中。请注意,我使用Boolean 来表示单个位而不是Char,因此Iterator[Bit](实际上是Iterator[Boolean])而不是String 来表示位序列。实现的想法基于grouped 方法,该方法将BitStream 转换为Bits 的流,分组为字节大小的组(最后一个组除外)。

恕我直言,与 Sarvesh Kumar Singh 答案相比,这种面向流的方法的主要优点是您不需要一次将整个文件加载到内存中或将整个编码文件存储在内存中。但是请注意,在这种情况下,您必须读取该文件两次:第一次构建EncodingMapping,第二次应用它。显然,如果文件足够小,您可以先将其加载到内存中,然后使用.toArray 调用将ByteStream 转换为Array[Byte]。但是如果您的文件很大,您可以只使用基于流的方法,并使用.foreach(b =&gt; out.write(b)) 之类的方式轻松地将ByteStream 保存到文件中

【讨论】:

    【解决方案2】:

    我认为这不会帮助您实现 Huffman 压缩目标,但回答您的问题:

    字符串到值

    String 转换为它所代表的值非常容易。

    val x: Int = "10101010".foldLeft(0)(_*2 + _.asDigit)
    

    注意:您必须在转换前检查格式(只有 1 和 0)和溢出(字符串太长)。

    文件价值

    有多种方法可以将数据写入文件。这是一个简单的。

    import java.io.{FileOutputStream, File}
    
    val fos = new FileOutputStream(new File("output.dat"))
    fos.write(x)
    fos.flush()
    fos.close()
    

    注意:您需要捕获任何抛出的错误。

    【讨论】:

    • "10101010".foldLeft(0)(_*2 + _.asDigit) 对于大多数大到足以成为任何有意义的霍夫曼编码的字符串都会失败。
    • @SarveshKumarSingh;当然,您是对的,但是,公平地说,我试图在@jemiloii 用修改后的标题、标签和开头段落“增强”(扭曲)它之前回答原始问题。而且我认为我非常清楚我要演示的内容(字符串->值转换)以及未解决的内容(实际的霍夫曼编码)。
    【解决方案3】:

    我将首先指定所有必需的导入,

    import java.io.{ File, FileInputStream, FileOutputStream}
    import java.nio.file.Paths
    
    import scala.collection.mutable.ArrayBuffer
    

    现在,我们将需要以下更小的单位来实现这一切,

    1 - 我们需要能够将我们的二进制字符串(例如"01010")转换为Array[Byte]

    def binaryStringToByteArray(binaryString: String) = {
      val byteBuffer = ArrayBuffer.empty[Byte]
    
      var byteStr = ""
      for (binaryChar <- binaryString) {
        if (byteStr.length < 7) {
          byteStr = byteStr + binaryChar
        }
        else {
          try{
            val byte = java.lang.Byte.parseByte(byteStr + binaryChar, 2)
            byteBuffer += byte
            byteStr = ""
          }
          catch {
            case ex: java.lang.NumberFormatException =>
              val byte = java.lang.Byte.parseByte(byteStr, 2)
              byteBuffer += byte
              byteStr = "" + binaryChar
          }
        }
      }
    
      if (!byteStr.isEmpty) {
        val byte = java.lang.Byte.parseByte(byteStr, 2)
        byteBuffer += byte
        byteStr = ""
      }
    
      byteBuffer.toArray
    }
    

    2 - 我们需要能够打开文件以在我们的小游戏中发球,

    def openFile(filePath: String): File = {
      val path = Paths.get(filePath)
      val file = path.toFile
    
      if (file.exists()) file.delete()
    
      if (!file.exists()) file.createNewFile()
    
      file
    }
    

    3 - 我们需要能够将字节写入文件,

    def writeBytesToFile(bytes: Array[Byte], file: File): Unit = {
      val fos = new FileOutputStream(file)
      fos.write(bytes)
      fos.close()
    }
    

    4 - 我们需要能够从文件中读回字节,

    def readBytesFromFile(file: File): Array[Byte] = {
      val fis = new FileInputStream(file)
      val bytes = new Array[Byte](file.length().toInt)
      fis.read(bytes)
      fis.close()
      bytes
    }
    

    5 - 我们需要能够将字节转换回我们的二进制字符串,

    def byteArrayToBinaryString(byteArray: Array[Byte]): String = {
      byteArray.map(b => b.toBinaryString).mkString("")
    }
    

    现在,我们已经准备好做我们想做的每一件事了,

    // lets say we had this binary string,
    scala> val binaryString = "00101110011010101010101010101"
    // binaryString: String = 00101110011010101010101010101
    
    // Now, we need to "pad" this with a leading "1" to avoid byte related issues
    scala> val paddedBinaryString = "1" + binaryString
    // paddedBinaryString: String = 100101110011010101010101010101
    
    // The file which we will use for this,
    scala> val file = openFile("/tmp/a_bit")
    // file: java.io.File = /tmp/a_bit
    
    // convert our padded binary string to bytes
    scala> val bytes = binaryStringToByteArray(paddedBinaryString)
    // bytes: Array[Byte] = Array(75, 77, 85, 85)
    
    // write the bytes to our file,
    scala> writeBytesToFile(bytes, file)
    
    // read bytes back from file,
    scala> val bytesFromFile = readBytesFromFile(file)
    // bytesFromFile: Array[Byte] = Array(75, 77, 85, 85)
    
    // so now, we have our padded string back,
    scala> val paddedBinaryStringFromFile = byteArrayToBinaryString(bytes)
    // paddedBinaryStringFromFile: String = 1001011100110110101011010101
    
    // remove that "1" from the front and we have our binaryString back,
    scala> val binaryStringFromFile = paddedBinaryString.tail
    // binaryStringFromFile: String = 00101110011010101010101010101
    

    注意:如果您想处理非常大的“二进制字符串”(超过几百万个字符)以提高性能甚至可用,您可能需要进行一些更改。例如 - 您需要开始使用 Streams 或 Iterators 而不是 Array[Byte]

    【讨论】:

    • 这段代码中有一些我不喜欢的地方,这让我提供了自己的答案。但我认为有一个非常棘手的地方值得明确提及:致电fis.read(bytes) 不可靠。 read 返回 int 是有原因的。真正棘手的部分是,当文件相对较小并且开始仅在大文件上产生奇怪的输出时,这段代码可能运行得很好。另请参阅stackoverflow.com/questions/326390/…
    • 我同意您的所有担忧。只是我想让这段代码尽可能简单和对初学者友好。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2023-03-14
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多