Java-高级主题-四-

72 阅读45分钟

Java 高级主题(四)

原文:Advanced Topics in Java: Core Concepts in Data Structures

协议:CC BY-NC-SA 4.0

七、使用文件

在本章中,我们将解释以下内容:

  • 文本文件和二进制文件的区别
  • 内部文件名和外部文件名的区别
  • 如何写一个程序来比较两个文本文件
  • try . . . catch构造
  • 如何对二进制文件进行输入/输出
  • 如何使用记录的二进制文件
  • 什么是随机存取文件
  • 如何从随机存取文件中创建和检索记录
  • 什么是索引文件
  • 如何使用索引更新随机存取文件

7.1 Java 中的输入/输出

Java 有广泛的用于执行输入/输出的类。我们已经使用System.inSystem.out分别从标准输入读取和写入标准输出。

例如,我们使用了以下代码从文件中读取数据:

        Scanner in = new Scanner(new FileReader("input.txt"));

此外,我们还使用了以下代码将输出发送到文件中,output.txt:

        PrintWriter out = new PrintWriter(new FileWriter("output.txt"));

我们处理过的文件都是文本文件(字符文件)。在本章中,我们将看到如何处理二进制文件。

7.2 文本和二进制文件 和

一个文本文件是组织成行的字符序列。从概念上讲,我们认为每行都以换行符结束。但是,根据主机环境的不同,可能会发生某些字符转换。例如,如果我们将换行符\n写到一个文件中,它可以被翻译成两个字符——一个回车符和一个换行符。

因此,书写的字符和存储在外部设备上的字符之间不一定一一对应。同样,文件中存储的字符数和读取的字符数之间也可能没有一一对应的关系。

一个二进制文件仅仅是一个字节序列,在输入或输出中没有字符转换。因此,这里的就是读取或写入的内容与文件中存储的内容一一对应。

除了可能的字符翻译之外,文本文件和二进制文件之间还有其他区别。为了说明,使用 2 个字节(16 位)存储一个short整数;数字3371被存储为00001101 00101011

如果我们把这个数写到一个文本文件中,它将被写成字符 3,接着是字符 3,接着是字符 7,接着是字符 1,总共占用 4 个字节。另一方面,我们可以简单地将这两个字节按原样写入二进制文件。

尽管我们仍然可以把它们看作是两个“字符”的序列,但是它们包含的值可能不代表任何有效的字符。其实在这种情况下,两个字节的十进制值是1343,解释为两个 ASCII 字符,分别是回车符(CR)和+

另一种理解方式是,通常,文本文件中的每个字节都包含一个人类可读的字符,而二进制文件中的每个字节都包含一个任意的位模式。二进制文件对于将数据从其内部表示形式直接写入外部设备(通常是磁盘文件)非常重要。

标准的输入和输出被认为是文本文件。磁盘文件可以创建为文本文件或二进制文件。我们将很快看到如何做到这一点。

7.3 内部与外部文件名

使用计算机的通常方式是通过它的操作系统。我们通常使用文字处理器或文本文件编辑器来创建和编辑文件。当我们创建一个文件时,我们给它一个名字,当我们需要对这个文件做任何事情时,我们就使用这个名字。这是操作系统识别文件的名称。

我们将这种名称称为外部文件名。(术语外部在这里表示“Java 程序的外部”)当我们写程序时,我们可能想要指定,比如说,从文件中读取数据。该程序将需要使用一个文件名,但是,出于几个原因,这个名称不应该是一个外部文件名。以下是主要原因:

  • 要读取的文件可能尚未创建。
  • 如果外部名称与程序相关联,程序将只能读取具有该名称的文件。如果数据在一个不同名称的文件中,要么改变程序,要么重命名文件。
  • 由于不同的操作系统有不同的文件命名规则,所以程序的可移植性较差。一个系统上的有效外部文件名在另一个系统上可能无效。
  • 由于这些原因,Java 程序使用内部文件名——我们通常使用 in 作为输入,out 作为输出。例如,当我们编写以下代码时,我们将内部名称in与外部文件input.txt相关联:
        Scanner in = new Scanner(new FileReader("input.txt"));

这是唯一提到外部文件名的语句。剩下的程序是用in来写的。当然,我们可以更灵活地这样写:

        Scanner in = new Scanner(new FileReader(fileName));

当程序运行时,我们在fileName中提供文件名,如下所示:

        System.out.printf("Enter file name: ");
        String fileName = kb.nextLine();  //Scanner kb = new Scanner(System.in);

这个例子也说明了如何在同一个程序中从键盘读取数据和文件。例如,kb.nextInt()将读取在键盘上键入的整数,in.nextInt()将从文件input.txt中读取一个整数。

7.4 示例:比较两个文件

考虑比较两个文件的问题。逐行进行比较,直到发现不匹配或其中一个文件结束。程序 P7.1 展示了我们如何解决这个问题。

程序 P7.1

        import java.io.*;
        import java.util.*;
        public class CompareFiles {
           public static void main(String[] args) throws IOException {
              Scanner kb = new Scanner(System.in);

              System.out.printf("First file? ");
              String file1 = kb.nextLine();
              System.out.printf("Second file? ");
              String file2 = kb.nextLine();

              Scanner f1 = new Scanner(new FileReader(file1));
              Scanner f2 = new Scanner(new FileReader(file2));

              String line1 = "", line2 = "";
              int numMatch = 0;

              while (f1.hasNextLine() && f2.hasNextLine()) {
                 line1 = f1.nextLine();
                 line2 = f2.nextLine();
                 if (!line1.equals(line2)) break;
                 ++numMatch;
              }
              if (!f1.hasNextLine() && !f2.hasNextLine())
                 System.out.printf("\nThe files are identical\n");
              else if (!f1.hasNextLine())   //first file ends, but not the second
                 System.out.printf("\n%s, with %d lines, is a subset of %s\n",
                             file1, numMatch, file2);
              else if (!f2.hasNextLine())   //second file ends, but not the first
                 System.out.printf("\n%s, with %d lines, is a subset of %s\n",
                             file2, numMatch, file1);
              else { //mismatch found
                 System.out.printf("\nThe files differ at line %d\n", ++numMatch);
                 System.out.printf("The lines are \n%s\n and \n%s\n", line1, line2);
              }
              f1.close();
              f2.close();
           } //end main

        } //end class CompareFiles

该程序执行以下操作:

  • 它提示输入要比较的文件的名称;如果任何文件不存在,将抛出一个FileNotFoundException
  • 它创建两个Scannerf1f2,每个文件一个。
  • 它使用hasNextLine来检查一个文件是否有更多的行需要读取;如果是true,至少还有一行要读,如果是false,则已经到了文件的末尾。
  • 变量numMatch计算匹配行的数量。读取每个文件中的一行。如果匹配,1加到numMatch上,读取另一对行。如果一个(或两个)文件结束,循环自然退出;如果出现不匹配,我们就break退出循环。

如果第一个文件包含以下内容:

one and one are two
two and two are four
three and three are six
four and four are eight
five and five are ten
six and six are twelve

第二个文件包含以下内容:

one and one are two
two and two are four
three and three are six
four and four are eight
this is the fifth line
six and six are twelve

该程序将打印以下内容:

The files differ at line 5
The lines are
five and five are ten
 and
this is the fifth line

7.5try . . . catch构造

当程序试图读取数据时,可能会出现错误。设备可能有问题,我们可能试图读取超出文件结尾的内容,或者我们要求读取的文件可能根本不存在。同样,当我们试图将数据写入文件时,设备可能被锁定或不可用,或者我们可能没有写权限。在这种情况下,Java 会抛出 IO(输入/输出)异常”。

每当一个方法有可能触发一个 I/O 错误时,无论是通过执行某个 I/O 操作本身还是通过调用一个触发错误的方法,Java 都要求该方法声明这一点。一种方法是在方法头中使用throws IOException,如下所示:

        public static void main(String[] args) throws IOException{

处理输入/输出错误的另一种方法是使用try . . . catch构造。假设一个程序包含以下语句:

        Scanner in = new Scanner(new FileReader("input.txt"));

运行时,如果程序找不到名为input.txt的文件,它将停止运行,并显示“文件未找到异常”错误消息。我们可以通过以下方式避免这种情况:

        try {
           Scanner in = new Scanner(new FileReader("input.txt"));
        }
        catch (IOException e) {
           System.out.printf("%s\n", e);
           System.out.printf("Correct the problem and try again\n");
           System.exit(1);
        }

try块由单词try后跟一个块组成(用大括号括起来的零个或多个语句)。Java 试图执行块中的语句。

catch部分由单词catch组成,后面是括号中的“异常类型”,再后面是一个块。在这个例子中,我们预计可能会抛出一个 I/O 异常,所以我们在catch之后使用IOException e。如果确实抛出了异常,则执行catch块中的语句。

在这个例子中,假设文件input.txt存在。Scanner in…语句将成功,程序将在的catch块后继续执行语句。但是如果这个文件不存在或者不可用,那么这个异常将被抛出并被部分 ?? 捕获。

当这种情况发生时,执行catch块中的语句(如果有的话)。Java 允许我们将任何语句放在一个catch块中。在这种情况下,我们打印异常对象e的内容和一条消息,程序退出。在没有文件input.txt的情况下运行时,该代码打印如下内容:

        java.io.FileNotFoundException: input.txt
        Correct the problem and try again

程序没有退出。如果省略了exit语句,程序将简单地继续执行catch块之后的语句(如果有的话)。如果我们愿意,我们也可以调用另一个方法来继续执行。

继续这个例子,考虑以下情况:

        try {
           Scanner in = new Scanner(new FileReader("input.txt"));
           n = in.nextInt();
        }

我们试图从文件中读取下一个整数。现在,很多事情都可能出错:文件可能不存在,文件中的下一项可能不是有效的整数,或者文件中可能没有“下一”项。这些将分别抛出“找不到文件”、“输入不匹配”和“没有这样的元素”异常。因为这些都是类Exception的子类,我们可以用下面的代码来捕捉它们:

        catch (Exception e) {
           System.out.printf("%s\n", e);
           System.out.printf("Correct the problem and try again\n");
           System.exit(1);
        }

当文件为空时,此代码打印如下内容:

        java.util.NoSuchElementException
        Correct the problem and try again

当文件包含数字5.7(非整数)时,它打印如下:

        java.util.InputMismatchException
        Correct the problem and try again

如果有必要,Java 允许我们单独捕捉每个异常。我们可以根据需要拥有任意多的catch构造。在本例中,我们可以编写以下内容:

        try {
           Scanner in = new Scanner(new FileReader("input.txt"));
           n = in.nextInt();
        }
        catch (FileNotFoundException e) {
           //code for file not found
        }
        catch (InputMismatchException e) {
           //code for “invalid integer” found
        }
        catch (NoSuchElementException e) {
           //code for “end of file” being reached
        }

有时候,catch子句的顺序很重要。假设我们希望将“文件未找到”与所有其他异常分开捕获。我们可能会忍不住这样写:

        try {
           Scanner in = new Scanner(new FileReader("input.txt"));
           n = in.nextInt();
        }
        catch (Exception e) {
           //code for all exceptions (except “file not found”, presumably)
        }
        catch (FileNotFoundException e) {
           //code for file not found
        }

这段代码甚至无法编译!Java 到最后一个catch的时候会抱怨FileNotFoundException已经被抓了。这是因为FileNotFoundExceptionException的子类。要解决这个问题,我们必须将catch (FileNotFoundException e) 放在 catch (Exception e)之前。

通常,子类异常必须出现在包含类之前。

7.6 二进制文件的输入/输出

如前所述,二进制文件包含的数据形式与数据的内部表示形式完全一致。例如,如果一个float变量占用了 4 个字节的内存,那么将它写入一个二进制文件只需要制作这 4 个字节的精确副本。另一方面,将其写入文本文件导致其被转换为字符形式,并且所获得的字符被存储在文件中。

通常,二进制文件只能从程序内部创建,并且其内容只能由程序读取。例如,列出一个二进制文件只会产生“垃圾”,有时还会产生错误。比较一个文本文件,它可以通过输入来创建,其内容可以被人类列出和阅读。但是,二进制文件具有以下优点:

  • 由于不需要进行数据转换,数据在二进制文件之间的传输速度比文本文件快得多;数据按原样读写。
  • 数组和结构等数据类型的值可以写入二进制文件。对于文本文件,必须写入单独的元素。
  • 存储在二进制文件中的数据通常比存储在文本文件中的数据占用更少的空间。例如,整数–25367(六个字符)在文本文件中占 6 个字节,但在二进制文件中只占 2 个字节。

7.6.1 DataOutputStreamDataInputStream

考虑从文本文件num.txt中读取整数,并将它们以内部形式写入(二进制)文件num.bin的问题。我们假设num.txt中的数字以0结束,并且0不会被写入二进制文件。这可以通过程序 P7.2 来完成。

程序 P7.2

     import java.io.*;
     import java.util.*;
     public class CreateBinaryFile {

        public static void main(String[] args) throws IOException {
           Scanner in = new Scanner(new FileReader("num.txt"));
           DataOutputStream out = new DataOutputStream(new FileOutputStream("num.bin"));
           int n = in.nextInt();
           while (n != 0) {
              out.writeInt(n);
              n = in.nextInt();
           }
           out.close();
           in.close();
        } //end main
     } //end class CreateBinaryFile

假设num.txt包含以下内容:

        25 18 47 96 73 89 82 13 39 0

运行程序 P7.2 时,数字(除0外)将以其内部形式存储在文件num.bin中。

程序 P7.2 中的新语句是:

        DataOutputStream out = new DataOutputStream(new FileOutputStream("num.bin"));

数据输出流允许程序将原始 Java 数据类型写入输出流。文件输出流是用于将数据写入文件的输出流。下面的构造函数创建一个连接到文件num.bin的输出流:

        new FileOutputStream("num.bin")

下面是DataOutputStream类中的一些方法。所有方法都写入基础输出流,所有值都从高字节开始写入(即从最高有效字节到最低有效字节)。

        void writeInt(int v)       //write an int
        void writeDouble(double v) //write a double value
        void writeChar(int v)      //write a char as a 2-byte value
        void writeChars(String s)  //write a string as a sequence of chars
        void writeFloat(float v)   //write a float value
        void writeLong(long v)     //write a long value
        void write(int b)          //write the low 8-bits of b

在程序 P7.2 中,out.writeInt(n)将整数n写入文件num.bin。如果你试图查看num.bin的内容,你看到的都是废话。只有程序可以读取并理解文件中的内容。

考虑一下程序 P7.3 ,它从文件num.bin中读取数字并打印出来。

程序 P7.3

        import java.io.*;
        public class ReadBinaryFile {
           public static void main(String[] args) throws IOException {
              DataInputStream in = new DataInputStream(new FileInputStream("num.bin"));
              int amt = 0;
              try {
                 while (true) {
                    int n = in.readInt();
                    System.out.printf("%d ", n);
                    ++amt;
                 }
              }
              catch (IOException e) { }
              System.out.printf("\n\n%d numbers were read\n", amt);
           } //end main
        } //end class ReadBinaryFile

如果num.bin包含来自程序 P7.2 的输出,那么程序 P7.3 产生如下输出:

25 18 47 96 73 89 82 13 39

9 numbers were read

程序 P7.3 中的新语句是:

        DataInputStream in = new DataInputStream(new FileInputStream("num.bin"));

数据输入流允许程序从作为数据输出流创建的输入流中读取原始 Java 数据类型。文件输入流是用于从文件中读取数据的输入流。下面的构造函数创建一个连接到文件num.bin的输入流:

        new FileInputStream("num.bin")

以下是DataInputStream类中的一些方法:

        int readInt()       //read 4 input bytes and return an int value
        double readDouble() //read 8 input bytes and return a double value
        char readChar()     //read a char as a 2-byte value
        void readFully(byte[] b) //read bytes and store in b until b is full
        float readFloat()        //read 4 input bytes and return a float value
        long readLong()          //read 8 input bytes and return a long value
        int skipBytes(int n)     //attempts to skip n bytes of data;
                                 //returns the number actually skipped

一般来说,这些方法用于从DataOutputStream读取使用相应“写入”方法写入的数据。

注意使用try . . . catch来读取数字,直到到达文件结尾。回想一下,文件中没有“数据结束”值,因此我们无法对此进行测试。while语句将连续从文件中读取。当到达文件结尾时,抛出一个EOFException。这是IOException的子类,因此被捕获。

catch块是空的,所以那里什么也没有发生。控制转到下面的语句,该语句打印读取的数字量。

7.6.2 记录的二进制文件

在上一节中,我们创建并读取了一个整数的二进制文件。我们现在讨论如何处理记录的二进制文件,其中一个记录可以包含两个或更多的字段。

假设我们想存储汽车零件的信息。现在,我们假设每个零件记录有两个字段——一个int零件号和一个double价格。假设我们有一个文本文件parts.txt,包含以下格式的零件数据:

        4250    12.95
        3000    17.50
        6699    49.99
        2270    19.25
        0

我们读取这些数据,并用程序 P7.4 创建一个二进制文件parts.bin

程序 P7.4

     import java.io.*;
     import java.util.*;
     public class CreateBinaryFile1 {
        public static void main(String[] args) throws IOException {
           Scanner in = new Scanner(new FileReader("parts.txt"));
           DataOutputStream out = new DataOutputStream(new FileOutputStream("parts.bin"));
           int n = in.nextInt();
           while (n != 0) {
              out.writeInt(n);
              out.writeDouble(in.nextDouble());
              n = in.nextInt();
           }
           in.close(); out.close();
        } //end main
     } //end class CreateBinaryFile1

parts.bin中的每条记录正好是 12 个字节(4 代表int + 8 代表double)。在本例中,有 4 条记录,因此文件的长度正好是 48 字节。我们知道第一条记录从字节 0 开始,第二条从字节 12 开始,第三条从字节 24 开始,第四条从字节 36 开始。下一条记录将从字节 48 开始。

在这个场景中,我们可以很容易地计算出记录 n 将从哪里开始;它将从字节数(n–1)* 12 开始。

为了给后面的内容做准备,我们将使用下面的Part类重写程序 P7.4 :

        class Part {
           int partNum;
           double price;

           public Part(int pn, double pr) {
              partNum = pn;
              price = pr;
           }

           public void printPart() {
              System.out.printf("\nPart number: %s\n", partNum);
              System.out.printf("Price: $%3.2f\n", price);
           }
        } //end class Part

程序 P7.5 从文本文件parts.txt中读取数据并创建二进制文件parts.bin

程序 P7.5

     import java.io.*;
     import java.util.*;
     public class CreateBinaryFile2 {
        static final int EndOfData = 0;

        public static void main(String[] args) throws IOException {
           Scanner in = new Scanner(new FileReader("parts.txt"));
           DataOutputStream fp = new DataOutputStream(new FileOutputStream("parts.bin"));

           Part part = getPartData(in);
           while (part != null) {
              writePartToFile(part, fp);
              part = getPartData(in);
           }

           in.close();
           fp.close();
        } //end main

        public static Part getPartData(Scanner in) {
           int pnum = in.nextInt();
           if (pnum == EndOfData) return null;
           return new Part(pnum, in.nextDouble());
        }

        public static void writePartToFile(Part part, DataOutputStream f) throws IOException {
           f.writeInt(part.partNum);
           f.writeDouble(part.price);
           part.printPart(); //print data on standard input
        } //end writePartToFile

     } //end class CreateBinaryFile2

     //class Part goes here

运行时,程序 P7.5 产生如下输出:

Part number: 4250
Price: $12.95

Part number: 3000
Price: $17.50

Part number: 6699
Price: $49.99

Part number: 2270
Price: $19.25

创建文件后,我们可以用下面的代码读取下一个Part记录:

        public static Part readPartFromFile(DataInputStream in) throws IOException {
           return new Part(in.readInt(), in.readDouble());
        } //end readPartFromFile

这假设以下声明:

        DataInputStream in = new DataInputStream(new FileInputStream("parts.bin"));

7.7 随机访问文件

在正常操作模式下,数据项是按照存储顺序从文件中读取的。当一个文件被打开时,人们会想到一个位于文件开头的假想指针。当从文件中读取项目时,该指针会按照读取的字节数移动。在任何时候,该指针指示下一个读(或写)操作将在哪里发生。

通常,这个指针是通过读或写操作隐式移动的。然而,Java 提供了将指针显式移动到文件中任何位置的工具。如果我们希望能够以随机顺序读取数据,而不是顺序读取数据,这是很有用的。

例如,考虑前面创建的部件二进制文件。每条记录的长度为 12 字节。如果第一条记录从字节 0 开始,那么第n?? 第条记录将从字节 12 开始(n–1)。假设我们想读取第 10 条记录,而不必读取前 9 条。我们算出第 10 个记录从字节 108 开始。如果我们可以将文件指针定位在字节 108,那么我们就可以读取第 10 个记录。

在 Java 中,RandomAccessFile类提供了处理随机访问文件的方法。以下语句声明parts.bin将被视为随机访问文件;rw是文件模式,意思是“读/写”——我们将被允许从文件*中读取,*向其中写入。

        RandomAccessFile fp = new RandomAccessFile("parts.bin", "rw");

如果我们想以只读模式打开文件,我们使用r而不是rw

最初,文件指针是 0,意味着它位于字节 0。

当我们从文件中读取数据或向文件中写入数据时,指针值会发生变化。在部件示例中,在我们读取(或写入)第一条记录后,指针值将是 12。在我们读取(或写入)第 5 个记录后,该值将是 60。不过,请注意,第 5 个记录从字节 48 开始。

在任何时候,fp.getFilePointer()返回指针的当前值。我们可以用seek将指针定位在文件中的任意字节。以下语句将指针定位在字节n处:

        fp.seek(n); //n is an integer; can be as big as a long integer

例如,我们可以将第 10 条记录读入一个Part变量,如下所示:

        fp.seek(108);  //the 10`th`record starts at byte 108
        Part part = new Part(fp.readInt(), fp.readDouble());

一般来说,我们可以这样来阅读第 n th 记录:

        fp.seek((n – 1) * 12);  //the n`th`record starts at byte (n – 1) * 12
        Part part = new Part(fp.readInt(), fp.readDouble());

我们注意到,12通常应该由一个符号常数代替,比如PartRecordSize

我们现在通过使用更真实的零件记录来扩展零件示例。假设每个零件现在有四个字段:六个字符的零件号、名称、库存量和价格。以下是一些示例数据:

        PKL070 Park-Lens 8 6.50
        BLJ375 Ball-Joint 12 11.95
        FLT015 Oil-Filter 23 7.95
        DKP080 Disc-Pads 16 9.99
        GSF555 Gas-Filter 9 4.50
        END

零件名被写成一个单词,因此可以用Scanner方法next读取。请注意,零件名称的长度并不相同。记住,为了使用随机存取文件,所有的记录必须具有相同的长度——这样我们可以计算出记录在文件中的位置。那么,如果零件名长度不同,我们如何创建零件记录的随机存取文件呢?

诀窍是使用相同的固定存储量来存储每个名称。例如,我们可以用 20 个字符存储每个名字。如果一个名字短于 20,我们用空格填充它来组成 20。如果它更长,我们将其截断为 20。但是,最好使用能够容纳最长名称的长度。

如果我们用 20 个字符存储一个名称,零件记录的大小是多少?在 Java 中,每个字符存储在 2 个字节中。因此,零件号(6 个字符)将占用 12 个字节,名称将占用 40 个字节,库存数量(整数)将占用 4 个字节,价格(双精度)将需要 8 个字节。这为每个记录提供了总共 64 个字节。

我们如下编写Part类:

        class Part {
           String partNum, name;
           int amtInStock;
           double price;

           public Part(String pn, String n, int a, double p) {
              partNum = pn;
              name = n;
              amtInStock = a;
              price = p;
           }

           public void printPart() {
              System.out.printf("Part number: %s\n", partNum);
              System.out.printf("Part name: %s\n", name);
              System.out.printf("Amount in stock: %d\n", amtInStock);
              System.out.printf("Price: $%3.2f\n", price);
           }

        } //end class Part

如果EndOfData的值为END,我们可以从零件文件中读取数据,假设它是以前的样本数据格式,如下所示:

        public static Part getPartData(Scanner in) {
           String pnum = in.next();
           if (pnum.equals(EndOfData)) return null;
           return new Part(pnum, in.next(), in.nextInt(), in.nextDouble());
        }

如果没有更多的数据,该方法返回null。否则,它返回一个包含下一部分数据的Part对象。

如果StringFixedLength表示存储零件名称的字符数,我们可以将名称写入文件f,如下所示:

        int n = Math.min(part.name.length(), StringFixedLength);
        for (int h = 0; h < n; h++) f.writeChar(part.name.charAt(h));
        for (int h = n; h < StringFixedLength; h++) f.writeChar(' ');

如果n表示名称的实际长度和StringFixedLength中较小的一个,我们首先将n字符写入文件。第二个for语句将空白写入文件,以补足所需的数量。注意,如果StringFixedLength比名字短,那么最后一个for不会写多余的空格。

要从文件中读取名称,我们将使用以下代码:

        char[] name = new char[StringFixedLength];
        for (int h = 0; h < StringFixedLength; h++) name[h] = f.readChar();
        String hold = new String(name, 0, StringFixedLength);

这正好将文件中的StringFixedLength个字符读入一个数组。这然后被转换成一个String并存储在hold中;hold.trim()将删除尾随空格,如果有的话。我们将使用hold.trim()来创建Part对象 read。

程序 P7.6 从文本文件parts.txt中读取数据,并创建随机存取文件parts.bin

程序 P7.6

   import java.io.*;
   import java.util.*;
   public class CreateRandomAccess {
      static final int StringFixedLength = 20;
      static final int PartNumSize = 6;
      static final int PartRecordSize = 64;
      static final String EndOfData = "END";

      public static void main(String[] args) throws IOException {
         Scanner in = new Scanner(new FileReader("parts.txt"));
         RandomAccessFile fp = new RandomAccessFile("parts.bin", "rw");
         Part part = getPartData(in);
         while (part != null) {
            writePartToFile(part, fp);
            part = getPartData(in);
         }
      } //end main

      public static Part getPartData(Scanner in) {
         String pnum = in.next();
         if (pnum.equals(EndOfData)) return null;
         return new Part(pnum, in.next(), in.nextInt(), in.nextDouble());
      } //end getPartData

      public static void writePartToFile(Part part, RandomAccessFile f) throws IOException {
         System.out.printf("%s %-11s %2d %5.2f %3d\n", part.partNum, part.name,
                                    part.amtInStock, part.price, f.getFilePointer());
         for (int h = 0; h < PartNumSize; h++) f.writeChar(part.partNum.charAt(h));
         int n = Math.min(part.name.length(), StringFixedLength);
         for (int h = 0; h < n; h++) f.writeChar(part.name.charAt(h));
         for (int h = n; h < StringFixedLength; h++) f.writeChar(' ');
         f.writeInt(part.amtInStock);
         f.writeDouble(part.price);
      } //end writePartToFile
   } //end class CreateRandomAccess

   //class Part goes here

当使用包含先前样本数据的parts.txt运行时,程序 P7.6 打印如下:

PKL070 Park-Lens    8  6.50   0
BLJ375 Ball-Joint  12 11.95  64
FLT015 Oil-Filter  23  7.95 128
DKP080 Disc-Pads   16  9.99 192
GSF555 Gas-Filter   9  4.50 256

每行的最后一个值是文件指针;这是存储记录的字节位置。使用格式规范%-11s在字段宽度11中左对齐打印零件名称(-表示左对齐)。

我们现在编写程序 P7.7 来测试文件是否被正确存储。它提示用户输入记录号,并打印相应的零件记录。

程序 P7.7

    import java.io.*;
    import java.util.*;
    public class ReadRandomAccess {
       static final int StringFixedLength = 20;
       static final int PartNumSize = 6;
       static final int PartRecordSize = 64;

       public static void main(String[] args) throws IOException {
          RandomAccessFile fp = new RandomAccessFile("parts.bin", "rw");
          Scanner kb = new Scanner(System.in);
          System.out.printf("\nEnter a record number: ");
          int n = kb.nextInt();
          while (n != 0) {
             fp.seek(PartRecordSize * (n - 1));
             readPartFromFile(fp).printPart();
             System.out.printf("\nEnter a record number: ");
             n = kb.nextInt();
          }
       } //end main

       public static Part readPartFromFile(RandomAccessFile f) throws IOException {
          String pname = "";
          for (int h = 0; h < PartNumSize; h++) pname += f.readChar();
          char[] name = new char[StringFixedLength];
          for (int h = 0; h < StringFixedLength; h++) name[h] = f.readChar();
          String hold = new String(name, 0, StringFixedLength);
          return new Part(pname, hold.trim(), f.readInt(), f.readDouble());
       } //end readPartFromFile
    } //end class ReadRandomAccess

    // class Part goes here

以下是程序 P7.7 的运行示例:

Enter a record number: 3
Part number: FLT015
Part name: Oil-Filter
Amount in stock: 23
Price: $7.95

Enter a record number: 1
Part number: PKL070
Part name: Park-Lens
Amount in stock: 8
Price: $6.50

Enter a record number: 4
Part number: DKP080
Part name: Disc-Pads
Amount in stock: 16
Price: $9.99
Enter a record number: 0

7.8 索引文件

上一节展示了如何在给定记录号的情况下检索零件记录。但是这不是检索记录的最自然的方式。更有可能的是,我们希望基于某个来检索记录,在本例中,是部件号。更自然的问题是,“我们有多少 BLJ375?”而不是“我们有多少记录 2?”接下来的问题是如何在给定零件号的情况下检索记录。

一种方法是使用索引。就像书籍索引让我们快速找到书中的信息一样,文件索引让我们快速找到文件中的记录。加载文件时会创建索引。以后,当记录被添加到文件中或从文件中删除时,它必须被更新。在我们的例子中,一个索引条目将由一个零件号和一个记录号组成。

我们将使用下面的类来创建索引:

        class Index {
           String partNum;
           int recNum;

           public Index(String p, int r) {
              partNum = p;
              recNum = r;
           }
        } //end class Index

我们将使用MaxRecords来表示我们将满足的最大记录数。我们声明一个数组index,如下所示:

        Index[] index = new Index[MaxRecords + 1];

我们将使用index[0].recNum来保存numRecords,即文件中存储的记录数。索引条目将存储在index[1]index[numRecords]中。

索引将按零件号有序保存。我们希望为以下记录创建索引:

        PKL070 Park-Lens    8  6.50
        BLJ375 Ball-Joint  12 11.95
        FLT015 Oil-Filter  23  7.95
        DKP080 Disc-Pads   16  9.99
        GSF555 Gas-Filter   9  4.50

我们假设记录按照给定的顺序存储在文件中。当读取并存储第一条记录时,索引将包含以下内容:

        PKL070   1

这意味着PKL070的记录是零件文件中的记录号 1。在第二个记录(BLJ375)被读取和存储后,索引将是这样的:

        BLJ375   2
        PKL070   1

请记住,我们是按照零件号来保存索引的。在第三个记录(FLT015)被读取和存储后,索引将是这样的:

        BLJ375   2
        FLT015   3
        PKL070   1

在第四个记录(DKP080)被读取和存储后,索引将是这样的:

        BLJ375   2
        DKP080   4
        FLT015   3
        PKL070   1

最后,在第五个记录(GSF555)被读取和存储之后,索引将是这样的:

        BLJ375   2
        DKP080   4
        FLT015   3
        GSF555   5
        PKL070   1

程序 P7.8 说明了如何按照描述创建索引。

程序 P7.8

   import java.io.*;
   import java.util.*;
   public class CreateIndex {
      static final int StringFixedLength = 20;
      static final int PartNumSize = 6;
      static final int PartRecordSize = 64;
      static final int MaxRecords = 100;
      static final String EndOfData = "END";

      public static void main(String[] args) throws IOException {
         RandomAccessFile fp = new RandomAccessFile("parts.bin", "rw");
         Index[] index = new Index[MaxRecords + 1];

         createMasterIndex(index, fp);
         saveIndex(index);
         printIndex(index);
         fp.close();
      } //end main

      public static void createMasterIndex(Index[] index,
                               RandomAccessFile f) throws IOException {
         Scanner in = new Scanner(new FileReader("parts.txt"));
         int numRecords = 0;
         Part part = getPartData(in);
         while (part != null) {
            int searchResult = search(part.partNum, index, numRecords);
            if (searchResult > 0)
               System.out.printf("Duplicate part: %s ignored\n", part.partNum);
            else { //this is a new part number; insert in location -searchResult
               if (numRecords == MaxRecords) {
                  System.out.printf("Too many records: only %d allowed\n", MaxRecords);
                  System.exit(1);
               }
               //the index has room; shift entries to accommodate new part
               for (int h = numRecords; h >= -searchResult; h--)
                     index[h + 1] = index[h];
               index[-searchResult] = new Index(part.partNum, ++numRecords);
               writePartToFile(part, f);
            }
            part = getPartData(in);
         } //end while
         index[0] = new Index("NOPART", numRecords);
         in.close();
      } //end createMasterIndex

      public static Part getPartData(Scanner in) {
         String pnum = in.next();
         if (pnum.equals(EndOfData)) return null;
         return new Part(pnum, in.next(), in.nextInt(), in.nextDouble());
      } //end getPartData

      public static void writePartToFile(Part part, RandomAccessFile f) throws IOException {
         for (int h = 0; h < PartNumSize; h++) f.writeChar(part.partNum.charAt(h));
         int n = Math.min(part.name.length(), StringFixedLength);
         for (int h = 0; h < n; h++) f.writeChar(part.name.charAt(h));
         for (int h = n; h < StringFixedLength; h++) f.writeChar(' ');
         f.writeInt(part.amtInStock);
         f.writeDouble(part.price);
      } //end writePartToFile

      public static void saveIndex(Index[] index) throws IOException {
         RandomAccessFile f = new RandomAccessFile("index.bin", "rw");
         int numRecords = index[0].recNum;
         //fill the unused index positions with dummy entries
         for (int h = numRecords+1; h <= MaxRecords; h++)
            index[h] = new Index("NOPART", 0);
         f.writeInt(MaxRecords);
         for (int h = 0; h <= MaxRecords; h++) {
            for (int i = 0; i < PartNumSize; i++)
                  f.writeChar(index[h].partNum.charAt(i));
            f.writeInt(index[h].recNum);
         }
         f.close();
      } //end saveIndex

      public static int search(String key, Index[] list, int n) {
      //searches list[1..n] for key. If found, it returns the location; otherwise
      //it returns the negative of the location in which key should be inserted.
         int lo = 1, hi = n;
         while (lo <= hi) {   // as long as more elements remain to consider
            int mid = (lo + hi) / 2;
            int cmp = key.compareToIgnoreCase(list[mid].partNum);
            if (cmp == 0) return mid;  // search succeeds
            if (cmp < 0) hi = mid - 1;   // key is 'less than' list[mid].partNum
            else lo = mid + 1;     // key is 'greater than' list[mid].partNum
         }
         return -lo;         // key not found; insert in location lo
      } // end search

      public static void printIndex(Index[] index) {
         System.out.printf("\nThe index is as follows: \n\n");
         int numRecords = index[0].recNum;
         for (int h = 1; h <= numRecords; h++)
            System.out.printf("%s %2d\n", index[h].partNum, index[h].recNum);
      } //end printIndex

   } //end class CreateIndex

   class Part {
      String partNum, name;
      int amtInStock;
      double price;

      public Part(String pn, String n, int a, double p) {
         partNum = pn;
         name = n;
         amtInStock = a;
         price = p;
      }

      public void printPart() {
         System.out.printf("Part number: %s\n", partNum);
         System.out.printf("Part name: %s\n", name);
         System.out.printf("Amount in stock: %d\n", amtInStock);
         System.out.printf("Price: $%3.2f\n", price);
      }
   } //end class Part

   class Index {
      String partNum;
      int recNum;

      public Index(String p, int r) {
         partNum = p;
         recNum = r;
      }
   } //end class Index

当读取零件号时,我们在索引中查找它。因为索引是按零件号排序的,所以我们使用二分搜索法来搜索它。如果零件号存在,则意味着该零件已经被存储,因此该记录被忽略。如果它不存在,这是一个新零件,所以它的记录存储在零件文件parts.bin中,假设我们还没有存储MaxRecords记录。

(在numRecords中)记录读取的记录数。然后将零件号和记录号插入到index数组的适当位置。

当所有记录都被存储后,索引被保存在另一个文件index.bin中。在保存之前,index(index[numRecords]之后的条目)的未使用部分被填充了虚拟记录。MaxRecords的值是发送到文件的第一个值。接下来是index[0]index[MaxRecords]。记住index[0].recNum包含了numRecords的值。

假设parts.txt包含以下内容:

        PKL070 Park-Lens 8 6.50
        BLJ375 Ball-Joint 12 11.95
        PKL070 Park-Lens 8 6.50
        FLT015 Oil-Filter 23 7.95
        DKP080 Disc-Pads 16 9.99
        GSF555 Gas-Filter 9 4.50
        FLT015 Oil-Filter 23 7.95
        END

当程序 P7.8 运行时,打印如下:

Duplicate part: PKL070 ignored
Duplicate part: FLT015 ignored

The index is as follows:

BLJ375  2
DKP080  4
FLT015  3
GSF555  5
PKL070  1

接下来,我们编写一个程序,通过首先从文件中读取索引来测试我们的索引。然后要求用户输入零件号,一次一个。对于每个零件,它会在索引中搜索零件号。如果找到,索引条目将指示零件文件中的记录号。使用记录号,检索零件记录。如果在索引中找不到零件号,则没有该零件的记录。程序显示为程序 P7.9 。

程序 P7.9

        import java.io.*;
        import java.util.*;
        public class UseIndex {
           static final int StringFixedLength = 20;
           static final int PartNumSize = 6;
           static final int PartRecordSize = 64;
           static int MaxRecords;

           public static void main(String[] args) throws IOException {
              RandomAccessFile fp = new RandomAccessFile("parts.bin", "rw");
              Index[] index = retrieveIndex();
              int numRecords = index[0].recNum;
              Scanner kb = new Scanner(System.in);
              System.out.printf("\nEnter a part number (E to end): ");
              String pnum = kb.next();
              while (!pnum.equalsIgnoreCase("E")) {
                 int n = search(pnum, index, numRecords);
                 if (n > 0) {
                    fp.seek(PartRecordSize * (index[n].recNum - 1));
                    readPartFromFile(fp).printPart();
                 }
                 else System.out.printf("Part not found\n");
                 System.out.printf("\nEnter a part number (E to end): ");
                 pnum = kb.next();
              } //end while
              fp.close();
           } //end main

           public static Index[] retrieveIndex() throws IOException {
              RandomAccessFile f = new RandomAccessFile("index.bin", "rw");
              int MaxRecords = f.readInt();
              Index[] index = new Index[MaxRecords + 1];
              for (int j = 0; j <= MaxRecords; j++) {
                 String pnum = "";
                 for (int i = 0; i < PartNumSize; i++) pnum += f.readChar();
                 index[j] = new Index(pnum, f.readInt());
              }
              f.close();
              return index;
           } //end retrieveIndex

           public static Part readPartFromFile(RandomAccessFile f) throws IOException {
              String pname = "";
              for (int h = 0; h < PartNumSize; h++) pname += f.readChar();
              char[] name = new char[StringFixedLength];
              for (int h = 0; h < StringFixedLength; h++) name[h] = f.readChar();
              String hold = new String(name, 0, StringFixedLength);
              return new Part(pname, hold.trim(), f.readInt(), f.readDouble());
           } //end readPartFromFile

           public static int search(String key, Index[] list, int n) {
           //searches list[1..n] for key. If found, it returns the location; otherwise
           //it returns the negative of the location in which key should be inserted.
              int lo = 1, hi = n;
              while (lo <= hi) {   // as long as more elements remain to consider
                 int mid = (lo + hi) / 2;
                 int cmp = key.compareToIgnoreCase(list[mid].partNum);
                 if (cmp == 0) return mid;  // search succeeds
                 if (cmp < 0) hi = mid - 1;   // key is 'less than' list[mid].partNum
                 else lo = mid + 1;     // key is 'greater than' list[mid].partNum
              }
              return -lo;         // key not found; insert in location lo
           } // end search

        } //end class UseIndex

        // Part and Index classes go here

以下是程序 P7.9 的运行示例:

Enter a part number (E to end): dkp080
Part number: DKP080
Part name: Disc-Pads
Amount in stock: 16
Price: $9.99

Enter a part number (E to end): GsF555
Part number: GSF555
Part name: Gas-Filter
Amount in stock: 9
Price: $4.50

Enter a part number (E to end): PKL060
Part not found

Enter a part number (E to end): pkl070
Part number: PKL070
Part name: Park-Lens
Amount in stock: 8
Price: $6.50

Enter a part number (E to end): e

请注意,可以使用任何大小写字母组合来输入零件号。

如果需要,我们可以使用索引按零件号顺序打印记录。我们只是按照记录在索引中出现的顺序打印它们。例如,使用我们的样本数据,我们有如下的索引:

    BLJ375    2
    DKP080    4
    FLT015    3
    GSF555    5
    PKL070    1

如果我们打印记录 2,然后是记录 4,然后是记录 3,然后是记录 5,然后是记录 1,我们将按零件号升序打印它们。这可以通过以下函数来完成:

   public static void printFileInOrder(Index[] index, RandomAccessFile f) throws IOException {
      System.out.printf("\nFile sorted by part number: \n\n");
      int numRecords = index[0].recNum;
      for (int h = 1; h <= numRecords; h++) {
         f.seek(PartRecordSize * (index[h].recNum - 1));
         readPartFromFile(f).printPart();
         System.out.printf("\n");
      } //end for
   } //end printFileInOrder

假设该函数被添加到程序 P7.9 中,并在检索到索引后用以下语句调用:

        printFileInOrder(index, fp);

将打印以下内容:

File sorted by part number:

Part number: BLJ375
Part name: Ball-Joint
Amount in stock: 12
Price: $11.95

Part number: DKP080
Part name: Disc-Pads
Amount in stock: 16
Price: $9.99

Part number: FLT015
Part name: Oil-Filter
Amount in stock: 23
Price: $7.95

Part number: GSF555
Part name: Gas-Filter
Amount in stock: 9
Price: $4.50

Part number: PKL070
Part name: Park-Lens
Amount in stock: 8
Price: $6.50

7.9 更新随机存取文件

文件中的信息通常不是静态的。它必须不时更新。对于我们的零件文件,我们可能希望更新它,以反映项目售出时的新库存数量,或者反映价格的变化。我们可能决定储存新零件,因此我们必须在文件中添加记录,并且我们可能停止销售某些产品,因此必须从文件中删除它们的记录。

添加新记录的方式与首先加载文件的方式类似。我们可以从逻辑上删除一条记录,方法是在索引中将它标记为已删除,或者简单地将它从索引中删除。稍后,当文件被重组时,记录可能被物理删除(即,不存在于新文件中)。但是我们怎样才能改变现有记录中的信息呢?为此,我们必须做到以下几点:

  1. 在文件中找到记录。
  2. 把它读入内存。
  3. 更改所需的字段。
  4. 将更新后的记录写入其来源文件的相同位置

这要求我们的文件以读写方式打开。假设文件已经存在,必须用模式rw打开。我们解释如何更新零件号存储在key中的记录。

首先,我们在索引中搜索key。如果找不到,则不存在该零件的记录。假设在位置k找到它。然后index[k].recNum给出它在零件文件中的记录号(n)。然后我们进行如下操作(为了清楚起见,省略了错误检查):

        fp.seek(PartRecordSize * (n - 1));
        Part part = readPartFromFile(fp);

该记录现在存储在变量part中。假设我们需要从库存数量中减去amtSold。这可以通过以下方式实现:

        if (amtSold > part.amtInStock)
           System.out.printf("Cannot sell more than you have: ignored\n");
        else part.amtInStock -= amtSold;

其他字段(除了零件号,因为它用于识别记录)可以类似地更新。当所有的更改都完成后,更新的记录部分保存在内存中。现在必须将它写回到文件中它原来的位置。这可以通过以下方式实现:

        fp.seek(PartRecordSize * (n - 1));
        writePartToFile(part, fp);

注意,我们必须再次调用seek,因为在第一次读取记录后,文件被定位在下一个记录的的开始处。在写入更新的记录之前,我们必须将它重新定位在刚刚读取的记录的开头。最终结果是更新的记录覆盖了旧的记录。

程序 P7.10 更新零件文件中记录的amtInStock字段。要求用户输入零件号和销售量。该程序使用二分搜索法在索引中搜索零件号。如果找到,则从文件中检索记录,在内存中更新,并写回文件。重复此过程,直到用户输入E

程序 P7.10

   import java.io.*;
   import java.util.*;
   public class UpdateFile {
      static final int StringFixedLength = 20;
      static final int PartNumSize = 6;
      static final int PartRecordSize = 64;
      static int MaxRecords;

      public static void main(String[] args) throws IOException {
         Scanner in = new Scanner(System.in);
         Index[] index = retrieveIndex();
         int numRecords = index[0].recNum;

         System.out.printf("\nEnter part number (E to end): ");
         String pnum = in.next();
         while (!pnum.equalsIgnoreCase("E")) {
            updateRecord(pnum, index, numRecords);
            System.out.printf("\nEnter part number (E to end): ");
            pnum = in.next();
         } //end while
      } //end main

      public static void updateRecord(String pnum, Index[] index, int max)throws IOException {
         Scanner in = new Scanner(System.in);
         RandomAccessFile fp = new RandomAccessFile("parts.bin", "rw");

         int n = search(pnum, index, max);
         if (n < 0) System.out.printf("Part not found\n");
         else {
            fp.seek(PartRecordSize * (index[n].recNum - 1));
            Part part = readPartFromFile(fp);
            System.out.printf("Enter amount sold: ");
            int amtSold = in.nextInt();
            if (amtSold > part.amtInStock)
               System.out.printf("You have %d: cannot sell more, ignored\n",
                        part.amtInStock);
            else {
               part.amtInStock -= amtSold;
               System.out.printf("Amount remaining: %d\n", part.amtInStock);
               fp.seek(PartRecordSize * (index[n].recNum - 1));
               writePartToFile(part, fp);
               System.out.printf("%s %-11s %2d %5.2f\n", part.partNum, part.name,
                                                       part.amtInStock, part.price);
            } //end if
         } //end if
         fp.close();
      } //end updateRecord

      public static Index[] retrieveIndex() throws IOException {
         RandomAccessFile f = new RandomAccessFile("index.bin", "rw");
         int MaxRecords = f.readInt();
         Index[] index = new Index[MaxRecords + 1];
         for (int j = 0; j <= MaxRecords; j++) {
            String pnum = "";
            for (int i = 0; i < PartNumSize; i++) pnum += f.readChar();
            index[j] = new Index(pnum, f.readInt());
         }
         f.close();
         return index;
      } //end retrieveIndex

      public static Part readPartFromFile(RandomAccessFile f) throws IOException {
         String pname = "";
         for (int h = 0; h < PartNumSize; h++) pname += f.readChar();
         char[] name = new char[StringFixedLength];
         for (int h = 0; h < StringFixedLength; h++) name[h] = f.readChar();
         String hold = new String(name, 0, StringFixedLength);
         return new Part(pname, hold.trim(), f.readInt(), f.readDouble());
      } //end readPartFromFile

      public static void writePartToFile(Part part, RandomAccessFile f) throws IOException {
         for (int h = 0; h < PartNumSize; h++) f.writeChar(part.partNum.charAt(h));
         int n = Math.min(part.name.length(), StringFixedLength);
         for (int h = 0; h < n; h++) f.writeChar(part.name.charAt(h));
         for (int h = n; h < StringFixedLength; h++) f.writeChar(' ');
         f.writeInt(part.amtInStock);
         f.writeDouble(part.price);
      } //end writePartToFile

      public static int search(String key, Index[] list, int n) {
      //searches list[1..n] for key. If found, it returns the location; otherwise
      //it returns the negative of the location in which key should be inserted.
         int lo = 1, hi = n;
         while (lo <= hi) {   // as long as more elements remain to consider
            int mid = (lo + hi) / 2;
            int cmp = key.compareToIgnoreCase(list[mid].partNum);
            if (cmp == 0) return mid;    // search succeeds
            if (cmp < 0) hi = mid - 1;   // key is 'less than' list[mid].partNum
            else lo = mid + 1;           // key is 'greater than' list[mid].partNum
         }
         return -lo;                     // key not found; insert in location lo
      } // end search

   } //end class UpdateFile

   // Part and Index classes go here

以下是程序 P7.10 的运行示例:

Enter part number (E to end): blj375
Enter amount sold: 2
Amount remaining: 10
BLJ375 Ball-Joint  10 11.95

Enter part number (E to end): blj375
Enter amount sold: 11
You have 10: cannot sell more, ignored

Enter part number (E to end): dkp080
Enter amount sold: 4
Amount remaining: 12
DKP080 Disc-Pads   12  9.99

Enter part number (E to end): gsf55
Part not found

Enter part number (E to end): gsf555
Enter amount sold: 1
Amount remaining: 8
GSF555 Gas-Filter   8  4.50

Enter part number (E to end): e

练习 7

  1. "r"打开的文件和用"rw"打开的文件有什么区别?

  2. 写一个程序来判断两个二进制文件是否相同。如果它们不同,打印它们不同的第一个字节数。

  3. 写一个程序来读取整数的(二进制)文件,对整数进行排序,并把它们写回同一个文件。假设所有的数字都可以存储在一个数组中。

  4. 重复(3 ),但假设在任何时候只有 20 个数字可以存储在内存中(在一个数组中)。提示:您将需要使用至少 2 个额外的临时输出文件。

  5. 写一个程序来读取两个整数排序文件,并将值合并到第三个排序文件中。

  6. 写一个程序来读取一个文本文件并产生另一个文本文件,其中所有的行都小于给定的长度。一定要在可感的地方断行;例如,避免断词或在行首放置孤立的标点符号。

  7. What is the purpose of creating an index for a file?

    以下是员工档案中的一些记录。这些字段是员工编号(键)、姓名、职称、电话号码、月薪和要扣除的税。

    STF425, Julie Johnson, Secretary, 623-3321, 2500, 600

    COM319, Ian McLean, Programmer, 676-1319, 3200, 800

    SYS777, Jean Kendall, Systems Analyst, 671-2025, 4200, 1100

    JNR591, Lincoln Kadoo, Operator, 657-0266, 2800, 700

    MSN815, Camille Kelly, Clerical Assistant, 652-5345, 2100, 500

    STF273, Anella Bayne, Data Entry Manager, 632-5324, 3500, 850

    SYS925, Riaz Ali, Senior Programmer, 636-8679, 4800, 1300

    假设记录按照给定的顺序存储在二进制文件中。

    1. 给定记录号,如何检索记录?

    2. 给定记录的键,如何检索记录?

    3. 加载文件时,创建一个索引,其中的键按给定的顺序排列。如何在这样的索引中搜索给定的键?

    4. As the file is loaded, create an index in which the keys are sorted. Given a key, how is the corresponding record retrieved?

      讨论在文件中添加和删除记录时,必须对索引进行哪些更改。

  8. 对于本章中讨论的“零件文件”应用,编写(I)添加新记录和(ii)删除记录的方法。

八、二叉树简介

在本章中,我们将解释以下内容:

  • 树和二叉树的区别
  • 如何执行二叉树的前序、有序和后序遍历
  • 如何在计算机程序中表示二叉树
  • 如何从给定的数据建立二叉树
  • 什么是二叉查找树以及如何构建
  • 如何编写一个程序来计算文章中单词的词频
  • 如何使用数组作为二叉树表示
  • 如何编写一些递归函数来获取关于二叉树的信息
  • 如何从二叉查找树中删除节点

8.1 树木

是一组有限的节点,以下两个条件都成立:

  • 有一个特别指定的节点叫做树的根。
  • 剩下的节点被分割成 m ≥ 0 个不相交的集合 T 1 ,T 2 ,…,T m ,这些集合中的每一个都是一棵树。

树 T 1 ,T 2 ,…,T m ,被称为根的子树。我们使用递归定义,因为递归是树结构的固有特性。图 8-1 说明了一棵树。按照惯例,根是画在顶端的,树是向下生长的。

9781430266198_Fig08-01.jpg

图 8-1 。一棵树

根是A。有三个子树分别以BCD为根。根在B的树有两个子树,根在C的树没有子树,根在D的树有一个子树。树的每个节点都是子树的根。

节点的是该节点的子树数。把它想象成离开节点的行数。例如,度(A ) = 3,度(C ) = 0,度(D ) = 1,度(G ) = 3。

我们使用术语兄弟来指代树的节点。比如父A有三个孩子,分别是BCD;父B有两个孩子,分别是EF;并且父节点D有一个子节点G,它有三个子节点:HIJ。请注意,一个节点可能是一个节点的子节点,但也可能是另一个节点的父节点。

兄弟节点是同一父节点的子节点。比如BCD是兄弟姐妹;EF是亲兄妹;并且HIJ是兄弟姐妹。

在树中,一个节点可以有几个子节点,但是除了根节点,只有一个父节点。根没有父级。换句话说,一个非根节点只有一行将引入

终端节点(也称为)是度为 0 的节点。分支节点是非终端节点。在图 8-1 的、CEFHIJ为叶子,而ABDG为分支节点。

树的是树中节点的个数。图 8-1 中的树有力矩 10。

一棵树的重量就是树上的叶子数。图 8-1 中的树权重为 6。

节点的级别(或深度)是从根到该节点的路径上必须经过的分支数。根的级别为 0。

在图 8-1 的树中、BCD为一级;EFG为二级;而HIJ处于 3 级。节点的级别是树中节点深度的度量。

一棵树的高度是树中的层数。图 8-1 中的树高 4。注意树的高度比它的最高高度多一。

如果子树 T 1 ,T 2 ,…,T m 的相对顺序重要,那么该树就是一棵有序 树。如果顺序不重要,树是面向

一个森林是零个或多个不相交的树 的集合,如图图 8-2 所示。

9781430266198_Fig08-02.jpg

图 8-2 。三棵不相交的树组成的森林

虽然人们对一般的树有些兴趣,但迄今为止最重要的树是二叉树。

8.2 二叉树

二叉树是非线性数据结构的经典例子——将其与线性列表进行比较,在该列表中,我们确定第一项、下一项和最后一项。二叉树是更一般的数据结构的特例,但它是最有用和最广泛使用的一种树。使用以下递归定义可以最好地定义二叉树:

一棵二叉树

  1. is empty

  2. 由一个根树和两个子树(一左一右)组成,每个子树都是一棵二叉树

这个定义的结果是一个节点总是有两个子树,其中任何一个都可能是空的。另一个结果是,如果一个节点有非空子树,区分它是在左边还是右边是很重要的。这里有一个例子:

9781430266198_unFig08-01.jpg

第一个有一个空的右边子树,而第二个有一个空的左边子树。但是,作为,它们是一样的。

下面是二叉树的例子。

这是一个只有一个节点的二叉树,根:

9781430266198_unFig08-02.jpg

这里有两个节点的二叉树:

9781430266198_unFig08-03.jpg

这里有三个节点的二叉树:

9781430266198_unFig08-04.jpg

以下是所有左子树和右子树都为空的二叉树:

9781430266198_unFig08-05.jpg

这是一棵二叉树,除了叶子,每个节点都有两个子树;这叫做完全二叉树 ??:

9781430266198_unFig08-06.jpg

这里是一个通用的二叉树:

9781430266198_unFig08-07.jpg

8.3 遍历二叉树

在许多应用中,我们希望以某种系统的方式访问二叉树的节点。现在,我们将认为“访问”只是在节点上打印信息。对于一棵有 n 个节点的树,有 n !访问它们的方法,假设每个节点被访问一次。

例如,对于一个有三个节点 A、B 和 C 的树,我们可以按以下任何顺序访问它们:ABC、ACB、BCA、BAC、CAB 和 CBA。并非所有这些命令都有用。我们将定义三种有用的方式——预排序、按序排序和后排序。

这是预购 t raversal :

  1. 访根。
  2. 按照预先的顺序遍历左边的子树。
  3. 按前序遍历右边的子树。

注意,遍历是递归定义的。在步骤 2 和 3 中,我们必须重新应用前序遍历的定义,即“访问根,等等”

这个树的前序遍历

9781430266198_unFig08-08.jpg

A B C

这个树的前序遍历

9781430266198_unFig08-09.jpg

C E F H B G A N J K

这是按序遍历 :

  1. 按顺序遍历左边的子树。
  2. 访根。
  3. 按顺序遍历右边的子树。

这里我们首先遍历左子树,然后是根,然后是右子树。

该树的有序遍历

9781430266198_unFig08-10.jpg

B A C

该树的有序遍历

9781430266198_unFig08-11.jpg

F H E B C A G J N K

这是后序遍历 :

  1. 按后顺序遍历左边的子树。
  2. 按后顺序遍历右边的子树。
  3. 访根。

这里,在访问根之前,我们遍历左右子树*。*

这个树的后序遍历

9781430266198_unFig08-12.jpg

B C A

这个树的后序遍历

9781430266198_unFig08-13.jpg

H F B E A J K N G C

请注意,遍历是从我们访问相对于左右子树的遍历的根的位置派生出它们的名称的。作为另一个例子,考虑可以表示以下算术表达式的二叉树:

          (54 + 37) / (725 * 13)

这是树:

9781430266198_unFig08-14.jpg

树的叶子包含操作数,分支节点包含操作符。给定一个包含运算符的节点,左子树表示第一个操作数,右子树表示第二个操作数。

前序遍历是:/ + 54 37 – 72 * 5 13

有序遍历是:54 + 37 / 72 – 5 * 13

后序遍历是:54 37 + 72 5 13 * - /

后序遍历可以与栈结合使用来计算表达式。算法如下:

        initialize a stack, S, to empty
        while we have not reached the end of the traversal
           get the next item, x
           if x is an operand, push it onto S
           if x is an operator
              pop its operands from S,
              apply the operator
              push the result onto S
           endif
        endwhile
        pop S; // this is the value of the expression

考虑后序遍历:54 37 + 72 5 13 * - /。其评估如下:

  1. 下一项是54;将54推到S上;S包含54
  2. 下一项是37;将37推到S上;S包含54 37(右上)。
  3. 下一项是+;从S弹出3754;将+应用到5437,给出91;将91推到S上;S包含91
  4. 下一项是72;将72推到S上;S包含91 72
  5. 接下来的项目是513;这些都推给了SS包含91 72 5 13
  6. 下一项是*;从S弹出135;将*应用到513,给出65;将65推到S上;S包含91 72 65
  7. 下一项是;从S弹出6572;将应用到7265,给出7;将7推到S上;S包含91 7
  8. 下一项是/;从S弹出791;将/应用到917,给出13;将13推到S上;S包含13
  9. 我们已经到达遍历的终点;我们弹出S,得到13—表达式的结果。

注意,当操作数从栈中弹出时,第一个弹出的是第二个操作数,第二个弹出的是第一个操作数。这对加法和乘法无关紧要,但对减法和除法很重要。

8.4 表示二叉树

二叉树的每个节点至少由三个字段组成:包含节点数据的字段、指向左子树的指针和指向右子树的指针。例如,假设存储在每个节点的数据是一个单词。我们可以从编写一个包含三个实例变量的类(TreeNode)和一个创建TreeNode对象的构造函数开始。

        class TreeNode {
           NodeData data;
           TreeNode left, right;

           TreeNode(NodeData d) {
              data = d;
              left = right = null;
           }
        }

为了保持选项的开放性,我们用一种我们称之为NodeData的通用数据类型来定义TreeNode。任何想要使用TreeNode的程序都必须提供自己对NodeData的定义。

例如,如果一个节点上的数据是一个整数,NodeData可以定义如下:

        class NodeData {
           int num;

           public NodeData(int n) {
              num = n;
           }
        } //end class NodeData

如果数据是字符,可以使用类似的定义。但是我们并不局限于单字段数据。可以使用任意数量的字段。稍后,我们将编写一个程序,对一篇文章中的单词进行频率统计。每个节点将包含一个单词及其频率计数。对于该计划,NodeData将包含以下内容:

        class NodeData {
           String word;
           int freq;

           public NodeData(String w) {
              word = w;
              freq = 0;
           }
        } //end class NodeData

除了树的节点,我们还需要知道树的根。请记住,一旦我们知道了根,我们就可以通过左右指针访问树中的所有节点。因此,二叉树仅由其根来定义。我们将开发一个BinaryTree类来处理二叉树。唯一的实例变量将是root。课程将按如下方式开始:

        class BinaryTree {
           TreeNode root;        // the only field in this class

           BinaryTree() {
              root = null;
           }
           //methods in the class
        } //end class BinaryTree

这个构造函数并不是真正必需的,因为当一个BinaryTree对象被创建时,Java 会将root设置为null。然而,我们包含它是为了强调,在空二叉树中,rootnull

如果你愿意,你可以把TreeNode类放在它自己的文件TreeNode.java中,并声明它为public。然而,在我们的程序中,我们将把TreeNode类和BinaryTree放在同一个文件中,因为它只被BinaryTree使用。为此,我们必须省略单词public并写成class TreeNode

8.5 构建二叉树

让我们写一个构建二叉树的函数。假设我们想要构建一个由单个节点组成的树,如下所示:

9781430266198_unFig08-15.jpg

数据将作为A @ @提供。每个@表示一个空指针的位置。

为了构建以下内容,我们将提供数据作为A B @ @ C @ @:

9781430266198_unFig08-16.jpg

每个节点紧跟着它的左子树,然后是它的右子树。

相比之下,为了构建以下内容,我们将提供数据作为A B @ C @ @ @

9781430266198_unFig08-17.jpg

C后面的两个@s表示其左右子树(空),最后一个@表示A的右子树(空)。

为了构建下面的内容,我们提供数据作为C E F @ H @ @ B @ @ G A @ @ N J @ @ K @ @

9781430266198_unFig08-18.jpg

给定这种格式的数据,下面的函数将构建树并返回指向其根的指针:

        static TreeNode buildTree(Scanner in) {
           String str = in.next();
           if (str.equals("@")) return null;
           TreeNode p = new TreeNode(new NodeData(str));
           p.left = buildTree(in);
           p.right = buildTree(in);
           return p;
        } //end buildTree

该函数将从与Scannerin相关的输入流中读取数据。它使用了NodeData的如下定义:

        class NodeData {
           String word;

           public NodeData(String w) {
              word = w;
           }
        } //end class NodeData

我们将从下面的构造函数中调用buildTree:

        public BinaryTree(Scanner in) {
           root = buildTree(in);
        }

假设一个用户类将其树数据存储在文件btree.in中。它可以用下面的代码创建一个二叉树bt:

        Scanner in = new Scanner(new FileReader("btree.in"));
        BinaryTree bt = new BinaryTree(in);

构建了树之后,我们应该想要检查它是否被正确地构建了。一种方法是执行遍历。假设我们想按预先的顺序打印bt的节点。如果能使用这样的语句就好了:

        bt.preOrder();

为此,我们需要在BinaryTree类中编写一个实例方法preOrder。该方法如下面的类列表所示。它还包括方法inOrderpostOrder。我们还保留了无参数构造函数,因此如果需要,用户可以从一个空的二叉树开始。

二叉树类

        import java.util.*;
        public class BinaryTree {
           TreeNode root;

           public BinaryTree() {
              root = null;
           }
           public BinaryTree(Scanner in) {
              root = buildTree(in);
           }

           public static TreeNode buildTree(Scanner in) {
              String str = in.next();
              if (str.equals("@")) return null;
              TreeNode p = new TreeNode(new NodeData(str));
              p.left = buildTree(in);
              p.right = buildTree(in);
              return p;
           } //end buildTree

           public void preOrder() {
              preOrderTraversal(root);
           }

           public void preOrderTraversal(TreeNode node) {
              if (node!= null) {
                 node.data.visit();
                 preOrderTraversal(node.left);
                 preOrderTraversal(node.right);
              }
           } //end preOrderTraversal

           public void inOrder() {
              inOrderTraversal(root);
           }

           public void inOrderTraversal(TreeNode node) {
              if (node!= null) {
                 inOrderTraversal(node.left);
                 node.data.visit();
                 inOrderTraversal(node.right);
              }
           } //end inOrderTraversal

           public void postOrder() {
              postOrderTraversal(root);
           }

           public void postOrderTraversal(TreeNode node) {
              if (node!= null) {
                 postOrderTraversal(node.left);
                 postOrderTraversal(node.right);
                 node.data.visit();
              }
           } //end postOrderTraversal

        } //end class BinaryTree

遍历都使用语句node.data.visit();。因为node.data是一个NodeData对象,所以NodeData类应该包含方法visit。在这个例子中,我们只打印节点上的值,所以我们写visit如下:

        public void visit() {
           System.out.printf("%s ", word);
        }

我们现在编写程序 P8.1 ,它构建了一个二叉树,并按照前序、中序和后序打印节点。像往常一样,我们可以将类BinaryTree声明为public,并将其存储在自己的文件BinaryTree.java中。我们也可以将类TreeNode声明为public,并将其存储在自己的文件TeeeNode.java中。但是,如果您更喜欢将整个程序放在一个文件BinaryTreeTest.java中,您可以省略单词public并在程序 P8.1 中指示的位置包含类TreeNodeBinaryTree

程序 P8.1

        import java.io.*;
        import java.util.*;
        public class BinaryTreeTest {

           public static void main(String[] args) throws IOException {
              Scanner in = new Scanner(new FileReader("btree.in"));
              BinaryTree bt = new BinaryTree(in);
              System.out.printf("\nThe pre-order traversal is: ");
              bt.preOrder();
              System.out.printf("\n\nThe in-order traversal is: ");
              bt.inOrder();
              System.out.printf("\n\nThe post-order traversal is: ");
              bt.postOrder();
              System.out.printf("\n\n");
              in.close();
           } // end main
        } //end class BinaryTreeTest

        class NodeData {
           String word;

           public NodeData(String w) {
              word = w;
           }

           public void visit() {
              System.out.printf("%s ", word);
           }
        } //end class NodeData

        // class TreeNode goes here

        // class BinaryTree goes here

如果btree.in包含C E F @ H @ @ B @ @ G A @ @ N J @ @ K @ @,那么程序 P8.1 构建如下树:

9781430266198_unFig08-19.jpg

然后,它打印如下遍历:

   The pre-order traversalis: C E F H B G A N J K

   The in-order traversalis: F H E B C A G J N K

   The post-order traversalis: H F B E A J K N G C

buildTree方法不限于单字符数据;可以使用任何字符串(不包含空格,因为我们使用%s来读取数据)。

例如,如果btree.in包含以下内容:

   hat din bun @ @ fan @ @ rum kit @ @ win @ @

然后程序 P8.1 构建如下树:

9781430266198_unFig08-20.jpg

然后,它打印如下遍历:

   The pre-order traversal is:  hat din bun fan rum kit win

   The in-order traversal is:   bun din fan hat kit rum win

   The post-order traversal is: bun fan din kit win rum hat

顺便说一下,注意二叉树的有序和前序遍历唯一地定义了该树。顺序内和顺序后都一样。然而,前序和后序并不唯一地定义树。换句话说,可能有两个不同的树 A 和 B,其中 A 的前序和后序遍历分别与 B 的前序和后序遍历相同。作为练习,举两个这样的树的例子。

8.6 二分搜索法树

考虑一个可能的二叉树,由图 8-3 所示的三个字母的单词组成。

9781430266198_Fig08-03.jpg

图 8-3 。二叉查找树和一些三个字母的单词

这是一种特殊的二叉树。它有这样的性质,给定任何一个节点,左子树中的单词比该节点上的单词“小”,右子树中的单词比该节点上的单词“大”。(此处,较小较大指字母顺序。)

这样的树叫做二叉查找树 (BST) 。它使用类似于数组二分搜索法的搜索方法来帮助搜索给定的键。

考虑对ria的搜索。从根开始,riaode相比较。由于riaode大(按字母顺序排列),我们可以得出结论,如果它在树中,那么它一定在右边的子树中。一定是这样,因为左子树中的所有节点都小于ode

跟随ode的右子树,我们接下来比较riatee。由于riatee小,我们沿着tee的左子树。然后我们比较riaria,搜索成功结束。

但是如果我们在搜索fun呢?

  1. funode小,所以我们走左边。
  2. funlea小,所以我们再往左走。
  3. fun大于era,所以必须向右走。

但是由于era的右子树是空的,我们可以断定fun不在树中。如果有必要将fun添加到树中,注意我们也已经找到了必须添加的地方。必须添加为era的右子树,如图图 8-4 所示。

9781430266198_Fig08-04.jpg

图 8-4 。添加乐趣后的 BST

因此,二叉查找树不仅方便了搜索,而且如果没有找到某个项目,也可以很容易地将其插入。它结合了二分搜索法的速度优势和链表的简单插入。

图 8-3 中绘制的树是七个给定单词的最佳二叉查找树。这意味着它是这些单词的最佳可能的树,从这个意义上说,没有更浅的二叉树可以从这些单词中建立。在包含这些单词的线性数组中,它会给出与二分搜索法相同的比较次数来查找一个键。

但这并不是这些词唯一可能的搜索树。假设单词一次出现一个,当每个单词出现时,它被添加到树中,使得树保持二叉查找树。最终构建的树将取决于单词出现的顺序。例如,假设单词按以下顺序出现:

        mac  tee  ode  era  ria  lea  vim

最初,树是空的。当mac进来的时候,就变成了树根。

  • 接下来是tee,并与mac进行比较。由于tee比较大,所以作为mac的右子树插入。
  • ode接下来并且大于mac,所以我们向右走;ode比 tee 小,所以作为tee的左子树插入。
  • era是 next,比mac小,所以作为mac的左子树插入。

到目前为止构建的树如图 8-5 所示。

9781430266198_Fig08-05.jpg

图 8-5 。添加 mac、tee、ode、era 后的 BST

  • ria是 next 并且大于mac,所以我们向右走;它比tee小,所以我们走左边;它大于ode,所以作为ode的右子树插入。

按照这个过程,lea作为era的右子树插入,vim作为tee的右子树插入,得到如图图 8-6 所示的最终树。

9781430266198_Fig08-06.jpg

图 8-6 。添加所有七个单词后的 BST

请注意,获得的树与最佳搜索树有很大不同。查找给定单词所需的比较次数也发生了变化。例如,ria现在需要四次比较;以前需要三个,lea现在需要三个,而以前需要两个。但也不全是坏消息;era现在需要两个,而以前需要三个。

可以证明,如果单词以随机顺序出现,那么给定单词的平均搜索时间大约是最佳搜索树的平均时间的 1.4 倍,即对于具有 n 个节点的树,1.4 log2n。

但是最坏的情况呢?如果单词按字母顺序排列,那么构建的树将如图 8-7 所示。

9781430266198_Fig08-07.jpg

图 8-7 。退化的树

搜索这样的树被简化为链表的顺序搜索。这种树叫做退化树 ??。某些单词的顺序会给一些非常不平衡的树。作为练习,按照下列单词顺序画出获得的树:

  • 我是来喝茶的
  • 我是来喝茶的麦克瑞德
  • 我来的时候是莱娅·泰里尔·麦克·奥德 vim era lea tee ria mac ode
  • lea mac 我来喝茶是笑得 ode

8.7 建造一座二叉查找树

我们现在编写一个函数,在二叉查找树中查找或插入一个项目。假设前面定义了TreeNodeBinaryTree,我们编写函数findOrInsert,它是BinaryTree类中的一个实例方法。该函数在树中搜索一个NodeData项目d。如果找到了,它将返回一个指向该节点的指针。如果没有找到,该项将被插入到树中适当的位置,函数将返回一个指向新节点的指针。

        public TreeNode findOrInsert(NodeData d) {
           if (root == null) return root = new TreeNode(d);
           TreeNode curr = root;
           int cmp;
           while ((cmp = d.compareTo(curr.data)) != 0) {
              if (cmp < 0) { //try left
                 if (curr.left == null) return   curr.left = new TreeNode(d);
                 curr = curr.left;
              }
              else { //try right
                 if (curr.right == null) return curr.right = new TreeNode(d);
                 curr = curr.right;
              }
           }
           //d is in the tree; return pointer to the node
           return curr;
        } //end findOrInsert

while条件中,我们使用表达式d.compareTo(curr.data)。这表明我们需要在NodeData类中编写一个compareTo方法来比较两个NodeData对象。该方法如下所示:

        public int compareTo(NodeData d) {
           return this.word.compareTo(d.word);
        }

它只是从String类中调用compareTo方法,因为NodeData只包含一个String对象。即使类中有其他字段,如果我们愿意,我们仍然可以决定基于word字段或任何其他字段来比较两个NodeData对象。

8.7.1 示例:词频统计

我们将通过编写一个程序来计算一篇文章中单词的出现频率,以此来说明目前为止所发展的思想。我们将把单词存储在二叉查找树中。在树中搜索每个输入的单词。如果没有找到这个单词,它将被添加到树中,并且它的频率计数被设置为 1。如果找到了这个词,那么它的频率计数就增加 1。在输入结束时,对树的有序遍历按字母顺序给出单词。

首先,我们必须定义NodeData类。这将包括两个字段(一个单词和它的频率),一个构造函数,一个给频率加 1 的函数,compareTovisit。这个类是这样的:

        class NodeData {
           String word;
           int freq;

           public NodeData(String w) {
              word = w;
              freq = 0;
           }
           public void incrFreq() {
              ++freq;
           }

           public int compareTo(NodeData d) {
              return this.word.compareTo(d.word);
           }

           public void visit() {
              WordFrequencyBST.out.printf("%-15s %2d\n", word, freq);
           }
        } //end class NodeData

请注意将频率增加 1 的方法。在visit中,我们使用对象WordFrequencyBST.out在一个节点打印数据。我们将很快编写类WordFrequencyBST,但是现在,注意我们将让它决定输出应该去哪里,并且out指定输出流。如果您愿意,您可以使用System.out.printf将结果发送到标准输出流。

构建搜索树的算法要点如下:

        create empty tree; set root to NULL
        while (there is another word) {
           get the word
           search for word in tree; insert if necessary and set frequency to 0
           add 1 to frequency //for an old word or a newly inserted one
        }
        print words and frequencies

对于我们的程序,我们将把一个单词定义为任何连续的大写或小写字母序列。换句话说,任何非字母都可以构成一个单词。特别是,空格和标点符号将分隔一个单词。如果in是一个Scanner对象,我们可以用这个语句指定这个信息:

        in.useDelimiter("[^a-zA-Z]+");  // ^ means "not"

方括号内的部分表示“任何不是小写或大写字母的字符”,而+表示这些字符中的一个或多个。

通常,Scanner使用空白来分隔使用next()读取的标记。但是,我们可以改变这一点,并指定我们希望用作分隔符的任何字符。例如,要使用冒号作为分隔符,我们可以这样写:

        in.useDelimiter(":");

当我们在程序中使用in.next()时,它将返回一个字符串,该字符串包含直到下一个冒号的字符,但不包括下一个冒号。要使用冒号或逗号作为分隔符,我们可以这样写:

        in.useDelimiter("[:,]"); //make a set using [ and ]

方括号表示一组。要使用冒号、逗号、句号或问号,我们这样写:

        in.useDelimiter("[:,\\.\\?]");

句号和问号是所谓的 meta 字符(用于特殊目的),所以我们必须使用转义序列来指定每一个。还有\?。回想一下,在一个字符串中,\是由\指定的。

如果我们想指定一个定界符是任何一个小写字母而不是的字符,我们这样写:

        in.useDelimiter("[^a-z]");  // ^ denotes negation, "not"

表达式a-z表示一个范围——从az

如果我们在右方括号后添加+,它表示“一个或多个”非小写字符的序列。因此,因为我们希望分隔符是“一个或多个”非字母(既不是大写也不是小写)的序列,所以我们这样写:

        in.useDelimiter("[^a-zA-Z]+");

我们现在编写程序 P8.2 来对文件wordFreq.in中的单词进行频数统计。它只是反映了我们之前概述的算法。

程序 P8.2

        import java.io.*;
        import java.util.*;
        public class WordFrequencyBST {
           static Scanner in;
           static PrintWriter out;

           public static void main(String[] args) throws IOException {
              in = new Scanner(new FileReader("wordFreq.in"));
              out = new PrintWriter(new FileWriter("wordFreq.out"));

              BinaryTree bst = new BinaryTree();

              in.useDelimiter("[^a-zA-Z]+");
              while (in.hasNext()) {
                 String word = in.next().toLowerCase();
                 TreeNode node = bst.findOrInsert(new NodeData(word));
                 node.data.incrFreq();
              }
              out.printf("\nWords        Frequency\n\n");
              bst.inOrder();
              in.close(); out.close();
           } // end main

        } //end class WordFrequencyBST

        class NodeData {
           String word;
           int freq;

           public NodeData(String w) {
              word = w;
              freq = 0;
           }
           public void incrFreq() {
              ++freq;
           }

           public int compareTo(NodeData d) {
              return this.word.compareTo(d.word);
           }

           public void visit() {
              WordFrequencyBST.out.printf("%-15s %2d\n", word, freq);
           }
        } //end class NodeData

        // class TreeNode goes here

        // class BinaryTree (with findOrInsert added) goes here

注意,inout被声明为static类变量。这对于in是不必要的,它本可以在main申报,因为它只在那里使用。然而,NodeData类的visit方法需要知道将输出发送到哪里,所以它需要访问out。我们通过将out声明为类变量来授予它访问权限。

由于findOrInsert需要一个NodeData对象作为它的参数,我们必须从word创建一个NodeData对象,然后在这个语句中调用它:

        TreeNode node = bst.findOrInsert(new NodeData(word));

搜索树的有序遍历按字母顺序产生单词。

假设文件wordFreq.in包含以下数据:

        If you can trust yourself when all men doubt you;
        If you can dream - and not make dreams your master;
        If you can talk with crowds and keep your virtue;
        If all men count with you, but none too much;
        If neither foes nor loving friends can hurt you;

当程序 P8.2 运行时,它将其输出发送到文件wordFreq.out。以下是输出:

Words        Frequency

all              2
and              2
but              1
can              4
count            1
crowds           1
doubt            1
dream            1
dreams           1
foes             1
friends          1
hurt             1
if               5
keep             1
loving           1
make             1
master           1
men              2
much             1
neither          1
none             1
nor              1
not              1
talk             1
too              1
trust            1
virtue           1
when             1
with             2
you              6
your             2
yourself         1

8.8 用父指针构建二叉树

我们已经看到了如何使用递归(使用栈实现)或显式栈来执行前序、按序和后序遍历。我们现在来看第三种可能性。首先,让我们构建树,使它包含“父”指针。

每个节点现在都包含一个附加字段—指向其父节点的指针。根的parent字段将是null。例如在图 8-8 所示的树中,H的父字段指向F , A的父字段指向G,G的父字段指向C

9781430266198_Fig08-08.jpg

图 8-8 。带有一些父指针的二叉树

为了表示这样的树,我们现在声明TreeNode如下:

        class TreeNode {
           NodeData data;
           TreeNode left, right, parent;

           public TreeNode(NodeData d) {
              data = d;
              left = right = parent = null;
           }
        } //end class TreeNode

我们现在可以将buildTree改写如下:

        public static TreeNode buildTree(Scanner in) {
           String str = in.next();
           if (str.equals("@")) return null;
           TreeNode p = new TreeNode(new NodeData(str));
           p.left = buildTree(in);
           if (p.left != null) p.left.parent = p;
           p.right = buildTree(in);
           if (p.right != null) p.right.parent = p;
           return p;
        } //end buildTree

在我们构建了节点p的左子树之后,我们检查它是否是null。如果是,那就没什么可进一步做的了。如果不是,并且q是它的根,我们将q.parent设置为p。类似的评论也适用于右边的子树。

有了父字段,我们可以在没有递归的情况下进行遍历,也没有递归所隐含的参数和局部变量的堆叠/拆分。例如,我们可以执行如下的有序遍历:

        get the first node in in-order; call it “node”
        while (node is not null) {
           visit node
           get next node in in-order
        }

给定树的非空根,我们可以按顺序找到第一个节点,如下所示:

        TreeNode node = root;
        while (node.left != null) node = node.left;

我们尽可能向左走。当我们不能再往前走的时候,我们已经按顺序到达了第一个节点。代码执行后,node将按顺序指向第一个节点。

要解决的主要问题如下:给定一个指向任何节点的指针,返回一个指向它的有序后继节点的指针,也就是说,如果有的话,它的有序后继节点之后。按顺序排列的最后一个节点将没有后继节点。

有两种情况需要考虑:

  1. If the node has a nonempty right subtree, then its in-order successor is the first node in the in-order traversal of that right subtree. We can find it with the following code, which returns a pointer to the in-order successor:

    if (node.right != null) {
       node = node.right;
       while (node.left != null) node = node.left;
       return node;
    }
    

    例如,考虑下面的树:

    9781430266198_unFig08-21.jpg

    通过向右一次(到N)然后尽可能向左(到J)找到G的有序后继。JG的继承人。

  2. If the node has an empty right subtree, then its in-order successor is one of its ancestors. Which one? It’s the lowest ancestor for which the given node is in its left subtree. For example, what is the in-order successor of B?

    我们来看看B的父母E。由于BE子树中,所以不是E

    然后我们看一下E的父节点C。由于E(因此,B)在C子树中,我们推断CB的有序后继。

    然而,请注意,K是顺序中的最后一个节点,没有后继节点。如果我们跟踪来自K的父指针,我们永远找不到左子树中有K的指针。在这种情况下,我们的函数将返回null

利用这些想法,我们将inOrderTraversal写成BinaryTree类中的一个实例方法,将inOrderSuccessor写成它调用的一个静态方法。

        public void inOrderTraversal() {
           if (root == null) return;
           //find first node in in-order
           TreeNode node = root;
           while (node.left != null) node = node.left;
           while (node != null) {
              node.data.visit(); //from the NodeData class
              node = inOrderSuccessor(node);
           }
        } //end inOrderTraversal

        private static TreeNode inOrderSuccessor(TreeNode node) {
           if (node.right != null) {
              node = node.right;
              while (node.left != null) node = node.left;
              return node;
           }
           //node has no right subtree; search for the lowest ancestor of the
           //node for which the node is in the ancestor's left subtree
           //return null if there is no successor (node is the last in in-order)
           TreeNode parent = node.parent;
           while (parent != null && parent.right == node) {
              node = parent;
              parent = node.parent;
           }
           return parent;
        } //end inOrderSuccessor

作为练习,编写类似的函数来执行前序和后序遍历。我们将在下一节编写一个测试inOrderTraversal的程序。

8.8.1 使用父指针构建二叉查找树

我们可以从BinaryTree类中修改findOrInsert函数来构建一个带有父指针的搜索树。这可以通过以下方式实现:

        public TreeNode findOrInsert(NodeData d) {
        //Searches the tree for d; if found, returns a pointer to the node.
        //If not found, d is added and a pointer to the new node returned.
        //The parent field of d is set to point to its parent.
           TreeNode curr, node;
           int cmp;

           if (root == null) {
              node = new TreeNode(d);
              node.parent = null;
              return root = node;
           }
           curr = root;

           while ((cmp = d.compareTo(curr.data)) != 0) {
              if (cmp < 0) { //try left
                 if (curr.left == null) {
                    curr.left  = new TreeNode(d);
                    curr.left.parent = curr;
                    return curr.left;
                 }
                 curr = curr.left;
              }
              else { //try right
                 if (curr.right == null)  {
                    curr.right = new TreeNode(d);
                    curr.right.parent = curr;
                    return curr.right;
                 }
                 curr = curr.right;
              } //end else
           } //end while
           return curr;  //d is in the tree; return pointer to the node
        } //end findOrInsert

当我们需要向树中添加一个节点(N)时,如果curr指向新节点将要悬挂的节点,我们只需将N的父字段设置为curr

我们可以用程序 P8.3 测试findOrInsertinOrderTraversal

程序 P8.3

        import java.io.*;
        import java.util.*;
        public class P8_3BinarySearchTreeTest {
           public static void main(String[] args) throws IOException {

              Scanner in = new Scanner(new FileReader("words.in"));

              BinaryTree bst = new BinaryTree();

              in.useDelimiter("[^a-zA-Z]+");
              while (in.hasNext()) {
                 String word = in.next().toLowerCase();
                 TreeNode node = bst.findOrInsert(new NodeData(word));
              }
              System.out.printf("\n\nThe in-order traversal is: ");
              bst.inOrderTraversal();
              System.out.printf("\n");
              in.close();
           } // end main

        } //end class P8_3BinarySearchTreeTest

        class NodeData {
           String word;

           public NodeData(String w) {
              word = w;
           }
           public int compareTo(NodeData d) {
              return this.word.compareTo(d.word);
           }

           public void visit() {
              System.out.printf("%s ", word);
           }
        } //end class NodeData

        class TreeNode {
           NodeData data;
           TreeNode left, right, parent;

           public TreeNode(NodeData d) {
              data = d;
              left = right = parent = null;
           }
        } //end class TreeNode

        //The BinaryTree class - only the methods relevant to this problem are shown
        class BinaryTree {
           TreeNode root;

           public BinaryTree() {
              root = null;
           }

            public void inOrderTraversal() {
              if (root == null) return;
              //find first node in in-order
              TreeNode node = root;
              while (node.left != null) node = node.left;
              while (node != null) {
                 node.data.visit(); //from the NodeData class
                 node = inOrderSuccessor(node);
              }
           } //end inOrderTraversal

           private static TreeNode inOrderSuccessor(TreeNode node) {
              if (node.right != null) {
                 node = node.right;
                 while (node.left != null) node = node.left;
                 return node;
              }
              //node has no right subtree; search for the lowest ancestor of the
              //node for which the node is in the ancestor's left subtree
              //return null if there is no successor (node is the last in in-order)
              TreeNode parent = node.parent;
              while (parent != null && parent.right == node) {
                 node = parent;
                 parent = node.parent;
              }
              return parent;
           } //end inOrderSuccessor

           //The method findOrInsert from this Section goes here
        } //end class BinaryTree

程序 P8.3 从文件words.in中读取单词,构建搜索树,并执行有序遍历以按字母顺序打印单词。例如,假设words.in包含以下内容:

        mac tee ode era ria lea vim

程序 P8.3 使用父指针构建以下二叉查找树:

9781430266198_unFig08-22.jpg

然后,它会打印以下内容:

The in-order traversal is: era lea mac ode ria tee vim

8.9 层级顺序遍历

除了前序、按序和后序,另一个有用的遍历是级序。这里,我们从根开始,一层一层地遍历树。在每一层,我们从左到右遍历节点。例如,假设我们有下面的树:

9781430266198_unFig08-23.jpg

它的层次顺序遍历是C E G B A N F J

为了执行层次顺序遍历,我们需要使用一个队列。以下算法说明了如何操作:

        add the root to the queue, Q
        while (Q is not empty) {
           remove item at the head of Q and store in p
           visit p
           if (left(p) is not null) add left(p) to Q
           if (right(p) is not null) add right(p) to Q
        }

对于前面的树,会出现以下情况:

  • C放在Q上。
  • Q不为空,所以移除并访问C;将EG添加到Q中,后者现在有E G
  • Q不为空,所以移除并访问E;把B加到Q,现在已经有了G B
  • Q非空;移除并访问G;将AN添加到Q中,后者现在有B A N
  • Q非空;移除并访问B;把F加到Q,现在已经有了A N F
  • Q非空;移除并访问AQ什么都不加,现在有了N F
  • Q非空;移除并访问N;把J加到Q,现在已经有了F J
  • Q非空;移除并访问FQ什么都不加,现在有了J
  • Q非空;移除并访问JQ什么都不加,现在是空的。
  • Q空;遍历结束时已经按顺序访问了节点C E G B A N F J

我们将需要以下内容来执行队列操作。首先,我们将类QueueData定义如下:

        public class QueueData {
           TreeNode node;

           public QueueData(TreeNode n) {
              node = n;
           }
        } //end class QueueData

接下来,我们定义类QNode:

        public class QNode {
           QueueData data;
           QNode next;

           public QNode(QueueData d) {
              data = d;
              next = null;
           }
        } //end class QNode

最后,这里是类Queue:

        public class Queue {
           QNode head = null, tail = null;

           public boolean empty() {
              return head == null;
           }

           public void enqueue(QueueData nd) {
              QNode p = new QNode(nd);
              if (this.empty()) {
                 head = p;
                 tail = p;
              }
              else {
                 tail.next = p;
                 tail = p;
              }
           } //end enqueue

           public QueueData dequeue() {
              if (this.empty()) {
                 System.out.printf("\nAttempt to remove from an empty queue\n");
                 System.exit(1);
              }
              QueueData hold = head.data;
              head = head.next;
              if (head == null) tail = null;
              return hold;
           } //end dequeue
        } //end class Queue

注意,如果你把QueueDataQueue或者使用Queue的程序放在同一个文件里,你必须省略public这个词。类似的评论也适用于QNode这个阶层。

使用QueueQueueData,我们可以将BinaryTree中的实例方法levelOrderTraversal编写如下:

        public void levelOrderTraversal() {
           Queue Q = new Queue();
           Q.enqueue(new QueueData(root));
           while (!Q.empty()) {
              QueueData temp = Q.dequeue();
              temp.node.data.visit();
              if (temp.node.left != null) Q.enqueue(new QueueData(temp.node.left));
              if (temp.node.right != null) Q.enqueue(new QueueData(temp.node.right));
           }
        } //end levelOrderTraversal

将所有这些放在一起,我们编写了程序 P8.4 ,它使用来自文件btree.in的数据构建了一棵树,并执行了一次层次顺序遍历。注意,为了将整个程序放在一个文件中,只有包含main的类被声明为public。在其他类中,只显示了与这个问题相关的方法。

程序 P8.4

        import java.io.*;
        import java.util.*;
        public class LevelOrderTest {
           public static void main(String[] args) throws IOException {

              Scanner in = new Scanner(new FileReader("btree.in"));
              BinaryTree bt = new BinaryTree(in);

              System.out.printf("\n\nThe level-order traversal is: ");
              bt.levelOrderTraversal();
              System.out.printf("\n");
              in.close();
           } // end main

        } //end class LevelOrderTest

        class NodeData {
           String word;

           public NodeData(String w) {
              word = w;
           }
           public void visit() {
              System.out.printf("%s ", word);
           }
        } //end class NodeData

        class TreeNode {
           NodeData data;
           TreeNode left, right, parent;

           public TreeNode(NodeData d) {
              data = d;
              left = right = parent = null;
           }
        } //end class TreeNode

        //The BinaryTree class - only the methods relevant to this problem are shown
        class BinaryTree {
           TreeNode root;

           public BinaryTree() {
              root = null;
           }

           public BinaryTree(Scanner in) {
              root = buildTree(in);
           }

          public static TreeNode buildTree(Scanner in) {
           String str = in.next();
              if (str.equals("@")) return null;
              TreeNode p = new TreeNode(new NodeData(str));
              p.left = buildTree(in);
              p.right = buildTree(in);
              return p;
           } //end buildTree

           public void levelOrderTraversal() {
              Queue Q = new Queue();
              Q.enqueue(new QueueData(root));
              while (!Q.empty()) {
                 QueueData temp = Q.dequeue();
                 temp.node.data.visit();
                 if (temp.node.left != null) Q.enqueue(new QueueData(temp.node.left));
                 if (temp.node.right != null) Q.enqueue(new QueueData(temp.node.right));
              }
           } //end levelOrderTraversal

        } //end class BinaryTree

        class QueueData {
           TreeNode node;

           public QueueData(TreeNode n) {
              node = n;
           }
        } //end class QueueData

        class QNode {
           QueueData data;
           QNode next;

           public QNode(QueueData d) {
              data = d;
              next = null;
           }
        } //end class QNode

        class Queue {
           QNode head = null, tail = null;

           public boolean empty() {
              return head == null;
           }

           public void enqueue(QueueData nd) {
              QNode p = new QNode(nd);
              if (this.empty()) {
                 head = p;
                 tail = p;
              }
              else {
                 tail.next = p;
                 tail = p;
              }
           } //end enqueue

           public QueueData dequeue() {
              if (this.empty()) {
                 System.out.printf("\nAttempt to remove from an empty queue\n");
                 System.exit(1);
              }
              QueueData hold = head.data;
              head = head.next;
              if (head == null) tail = null;
              return hold;
           } //end dequeue

        } //end class Queue

假设文件btree.in包含以下内容:

        C E @ B F @ @ @ G A @ @ N J @ @ @

程序 P8.4 将构建本节开头所示的树,并打印以下内容:

The level-order traversal is: C E G B A N F J

8.10 一些有用的二叉树函数 s

我们现在向你展示如何编写一些函数(在类BinaryTree中)来返回关于二叉树的信息。第一种方法计算树中节点的数量:

        public int numNodes() {
           return countNodes(root);
        }

        private int countNodes(TreeNode root) {
           if (root == null) return 0;
           return 1 + countNodes(root.left) + countNodes(root.right);
        }

如果bt是二叉树,bt.numNodes()将返回树中节点的个数。对节点的计数被委托给private功能countNodes

下一个函数返回树中叶子的数量:

        public int numLeaves() {
           return countLeaves(root);
        }

        private int countLeaves(TreeNode root) {
           if (root == null) return 0;
           if (root.left == null && root.right == null) return 1;
           return countLeaves(root.left) + countLeaves(root.right);
        }

下一个返回树的高度:

        public int height() {
           return numLevels(root);
        }

        private int numLevels(TreeNode root) {
           if (root == null) return 0;
           return 1 + Math.max(numLevels(root.left), numLevels(root.right));
        }

Math.max返回两个参数中较大的一个。

建议您在一些样本树上运行这些函数,以验证它们确实返回了正确的值。

8.11 二叉查找树删除

考虑从二叉查找树(BST)中删除一个节点以便它仍然是 BST 的问题。有三种情况需要考虑:

  1. 该节点是一个叶子。
  2. (a)该节点没有左子树。 (b)节点没有右边的子树。
  3. 该节点具有非空的左右子树。

我们使用图 8-9 所示的 BST 来说明这些情况。

9781430266198_Fig08-09.jpg

图 8-9 。一个二叉查找树

案例 1 很简单。例如,要删除P,我们只需将N的右边子树设置为 null。案例 2 也很简单。为了删除A(没有左子树),我们用它的右子树C代替它。为了删除F(没有右边的子树),我们用它的左边的子树A代替它。

情况 3 有点困难,因为我们必须考虑如何处理悬挂在节点上的两个子树。比如我们怎么删除L?一种方法是用它的有序后继者N替换L,其中必须有一个空的左子树。为什么呢?因为根据定义,一个节点的有序后继节点是其右子树中的第一个节点(按顺序)。并且这个第一个节点(在任何树中)是通过尽可能向左走找到的。

由于N没有左子树,我们将把它的左链接设置为L的左子树。我们将N(本例中为R)的父节点的左侧链接设置为指向N的右侧子树P。最后我们将N的右链接设置为指向L的右子树,给出图 8-10 所示的树。

9781430266198_Fig08-10.jpg

图 8-10 。在图 8-9 中删除 L 后的 BST

另一种方式是想象节点N的内容被复制到节点L中。并将N(也就是R))的父节点的左边链接设置为指向N(也就是P)的右边子树。

在我们的算法中,我们将把要删除的节点视为一个子树的根。我们将删除根,并返回一个指向重建树的根的指针。

pg251-252.jpg

假设我们用指向节点L ( 图 8-9 )的指针调用deleteNode作为自变量。该函数将删除L,并返回一个指向以下树的指针:

9781430266198_unFig08-28.jpg

由于LH的右子树,我们现在可以将H的右子树设置为指向这棵树的根N

8.12 以二叉树表示的数组

一棵完全二叉树是这样的树,其中每个非叶子节点有两个非空子树,并且所有的叶子都在同一层。图 8-11 显示了一些完整的二叉树。

9781430266198_Fig08-11.jpg

图 8-11 。完全二叉树

第一个是高度为 1 的完全二叉树,第二个是高度为 2 的完全二叉树,第三个是高度为 3 的完全二叉树。对于一棵高度为 n 的完全二叉树,树中的节点数为 2n-1。

考虑第三棵树。让我们按照图 8-12 所示对节点进行编号。

9781430266198_Fig08-12.jpg

图 8-12 。逐层对节点进行编号

从根节点的 1 开始,我们按照从上到下和从左到右的顺序对每一层的节点进行编号。

注意,如果一个节点具有标签 n ,则其左子树具有标签 2 n ,其右子树具有标签 2 n + 1。

如果节点存储在数组T[1..7]中,如下所示:

9781430266198_unFig08-29.jpg

然后

  • T[1]是根。
  • T[i]的左子树是 T[2i]如果 2i <= 7,否则为null
  • T[i]的右子树是 T[2i+1]如果 2i+1 <= 7,否则为null
  • T[i]的父代是 Ti/2。

基于此,数组是一个完整二叉树的表示。换句话说,给定数组,我们可以很容易地构造它所代表的二叉树。

如果数组中的元素个数为 2n–1,对于某些 n ,则该数组表示一棵完整的二叉树。如果元素的数量是某个其他值,数组表示一个几乎完整的二叉树。

一棵几乎完全二叉树是这样一棵树,其中:

  • 除了最低层之外,所有层都被完全填满。
  • 最底层的节点(所有叶子)尽可能靠左。

如果节点如前所示进行编号,那么所有的叶子将被标上从n/2+1n的连续编号。最后一个非叶节点将具有标签n/2。例如,考虑如图 8-13 中所示的有十个节点的树。

9781430266198_Fig08-13.jpg

图 8-13 。一个由十个节点组成的树,逐层标记

注意叶子的编号是从 6 到 10。例如,如果HB的右子树而不是左子树,那么该树将不是“几乎完整的”,因为最底层的叶子不会“尽可能地靠左”

以下大小为 10 的数组可以代表这个几乎完整的二叉树:

9781430266198_unFig08-30.jpg

一般而言,如果树由数组T[1..n]表示,则以下成立:

  • T[1]是根。
  • T[i]的左子树是 T[2i]如果 2i <= n,否则为 null。
  • T[i]的右子树是 T[2i+1]如果 2i+1 <= n,否则为 null。
  • T[i]的父代是 Ti/2。

从另一个角度来看,确实有一个几乎完整的二叉树,有 n 个节点,大小为 n 的数组表示这棵树。

几乎完整的二叉树中没有“洞”;没有空间在现有节点之间添加节点。添加节点的唯一位置是在最后一个节点之后。

例如,图 8-14 不是“几乎完整的”,因为在B的右边子树上有一个“洞”。

9781430266198_Fig08-14.jpg

图 8-14 。空的 B 的右子树使得这不是“几乎完全的”

有了洞,A(在位置 6)的左子树是不是现在在位置 62 = 12,右子树不在位置 62+1 =13。这种关系只有在树几乎完成时才成立。

给定一个表示具有 n 个节点的几乎完整的二叉树的数组T[1..n],我们可以通过调用下面的函数inOrder(1, n)来执行树的有序遍历:

        public static void inOrder(int h, int n) {
           if (h <= n) {
              inOrder(h * 2, n);
              visit(h); //or visit(T[h]), if you wish
              inOrder(h * 2 + 1, n);
           }
        } //end inOrder

我们可以为前序和后序遍历编写类似的函数。

与完全二叉树相比,完全二叉树是这样一种树,其中除了一片叶子之外,每个节点都有恰好两个非空子树。图 8-15 是一个完全二叉树的例子。

9781430266198_Fig08-15.jpg

图 8-15 。完整的二叉树

注意,一棵完整的二叉树总是满的,但是如图图 8-15 所示,一棵完整的二叉树不一定是完整的。几乎完整的二叉树可能是满的,也可能不是满的。

图 8-16 中的树几乎完整但不完整(G一个非空子树)。

9781430266198_Fig08-16.jpg

图 8-16 。几乎完全但不完全的二叉树

然而,如果节点A被移除,树将几乎完成并且满。

在下一章,我们将解释如何通过解释一个几乎完整的二叉树来排序一个数组。

练习 8

  1. 二叉树由一个整数键字段和指向左子树、右子树和父树的指针组成。编写构建树所需的声明,并编写创建空树的代码。

  2. Each node of a binary tree has fields left, right, key, and parent.

    编写一个函数来返回任意给定节点x的有序后继节点。提示:如果节点x的右子树为空,并且x有一个后继y,那么yx的最低祖先,它的子树中包含x

    编写一个函数来返回任何给定节点x的前序后继节点。

    编写一个函数来返回任意给定节点x的后序后继节点。

    使用这些函数,编写函数来执行给定二叉树的有序、前序和后序遍历。

  3. 假设树存储在一个数组中,做练习 2。

  4. 写一个函数,给定一个二叉查找树的根,删除最小的节点,并返回一个指向重建树的根的指针。

  5. 写一个函数,给定一个二叉查找树的根,删除最大的节点,并返回一个指向重建树的根的指针。

  6. 写一个函数,给定二叉查找树的根,删除根并返回一个指向重建树的根的指针。写出用(I)它的有序后继者和(ii)它的有序前趋者替换根的函数。

  7. 画一个五个节点的非退化二叉树,使得前序和层次序遍历产生相同的结果。

  8. 写一个函数,给定二叉树的根,返回树的,也就是任意级别的最大节点数。

  9. 二叉查找树包含整数。对于以下每个序列,说明它是否可能是在搜索数字36时检查的值序列。如果不能,说明原因。

    7 25 42 40 33 34 39 36
    92 22 91 24 89 20 35 36
    95 20 90 24 92 27 30 36
    7 46 41 21 26 39 37 24 36
    
  10. Draw the binary search tree (BST) obtained for the following keys assuming they are inserted in the following order: 56 30 61 39 47 35 75 13 21 64 26 73 18.

对于前面的键,有一个几乎完整的 BST。画出来。
按照产生几乎完整 BST 的顺序列出这些键。

假设几乎完整的树存储在一维数组`num[1..13]`中,写一个递归函数,用于按后序打印整数。

11. An imaginary “external” node is attached to each null pointer of a binary tree of n nodes. How many external nodes are there?

如果 **I** 是原树节点的层数之和, **E** 是外部节点的层数之和,证明**E****I**= 2*n*。( **I** 称为*内部路径长度*。)

写一个递归函数,给定一个二叉树的根,返回 **I** 。

写一个非递归函数,给定二叉树的根,返回 **I**

12. Draw the binary tree whose in-order and post-order traversals of the nodes are as follows:

按顺序:`G D P K E N F A T L`

后期订单:`G P D K F N T A L E`

13. Draw the binary tree whose pre-order and in-order traversals of the nodes are as follows:

预购:`N D G K P E T F A L`

按顺序:`G D P K E N F A T L`

14. 画两个不同的二叉树,使得一棵树的前序和后序遍历与另一棵树的前序和后序遍历相同。 15. 编写一个递归函数,在给定二叉树的根和一个键的情况下,使用(I)前序、(ii)按序和(iii)后序遍历来搜索该键。如果找到,返回包含该键的节点;否则,返回null。 16. Store the following integers in an array bst[1..15] such that bst represents a complete binary search tree:

`34 23 45 46 37 78 90 2 40 20 87 53 12 15 91`

17. 二叉查找树的每个节点包含三个字段——leftrightdata——具有它们通常的含义;data是正整数字段。写一个有效的函数,给定树的根和key,返回树中大于key最小的数。如果没有这个数字,则返回-1。** 18. Write a program that takes a Java program as input and outputs the program, numbering the lines, followed by an alphabetical cross-reference listing of all user identifiers; that is, a user identifier is followed by the numbers of all lines in which the identifier appears. If an identifier appears more than once in a given line, the line number must be repeated the number of times it appears.

交叉引用列表必须*而不是*包含 Java 保留字、字符串中的单词或注释中的单词。