遍历数组

Java 1.5中增强的for循环详解
本文深入探讨了Java 1.5引入的增强for循环,包括其用法、性能考虑、语法特性、类型兼容性、迭代器接口的使用、以及自定义可迭代对象的实现。详细解释了如何在遍历数组、集合等对象时简化代码,并通过泛型机制控制循环变量的类型。

J2SE 1.5提供了另一种形式的for循环。借助这种形式的for循环,可以用更简单地方式来遍历数组和Collection等类型的对象。本文介绍使用这种循环的具体方式,说明如何自行定义能被这样遍历的类,并解释和这一机制的一些常见问题。

Java程序中,要逐一处理”——或者说,遍历”——某一个数组或Collection中的元素的时候,一般会使用一个for循环来实现(当然,用其它种类的循环也不是不可以,只是不知道是因为for这个词的长度比较短,还是因为for这个词的含义和这种操作比较配,在这种时候for循环比其它循环常用得多)。

对于遍历数组,这个循环一般是采取这样的写法:

清单1:遍历数组的传统方式

/* 建立一个数组 */
int[] integers = {1, 2, 3, 4};
/*
开始遍历 */
for (int j = 0; j < integers.length; j++) {
     int i = integers[j];
     System.out.println(i);
}

而对于遍历Collection对象,这个循环则通常是采用这样的形式:

清单2:遍历Collection对象的传统方式

/* 建立一个Collection */
String[] strings = {"A", "B", "C", "D"};
Collection stringList = java.util.Arrays.asList(strings);
/*
开始遍历 */
for (Iterator itr = stringList.iterator(); itr.hasNext();) {
     Object str = itr.next();
     System.out.println(str);
}

而在Java语言的最新版本——J2SE 1.5中,引入了另一种形式的for循环。借助这种形式的for循环,现在可以用一种更简单地方式来进行遍历的工作。

1. 第二种for循环

不严格的说,Java的第二种for循环基本是这样的格式:

for (循环变量类型 循环变量名称 : 要被遍历的对象) 循环体

借助这种语法,遍历一个数组的操作就可以采取这样的写法:

清单3:遍历数组的简单方式

/* 建立一个数组 */
int[] integers = {1, 2, 3, 4};
/*
开始遍历 */
for (int i : integers) {
     System.out.println(i);/*
依次输出“1”“2”“3”“4” */
}

这里所用的for循环,会在编译期间被看成是这样的形式:

清单4:遍历数组的简单方式的等价代码

/* 建立一个数组 */
int[] integers = {1, 2, 3, 4};
/*
开始遍历 */
for (int
变量名甲 = 0; 变量名甲 < integers.length; 变量名甲++) {
     System.out.println(integers[
变量名甲]);/* 依次输出“1”“2”“3”“4” */
}

这里的变量名甲是一个由编译器自动生成的不会造成混乱的名字。

而遍历一个Collection的操作也就可以采用这样的写法:

清单5:遍历Collection的简单方式

/* 建立一个Collection */
String[] strings = {"A", "B", "C", "D"};
Collection list = java.util.Arrays.asList(strings);
/*
开始遍历 */
for (Object str : list) {
     System.out.println(str);/*
依次输出“A”“B”“C”“D” */
}

这里所用的for循环,则会在编译期间被看成是这样的形式:

清单6:遍历Collection的简单方式的等价代码

/* 建立一个Collection */
String[] strings = {"A", "B", "C", "D"};
Collection stringList = java.util.Arrays.asList(strings);
/*
开始遍历 */
for (Iterator
变量名乙 = list.iterator(); 变量名乙.hasNext();) {
    Object str =
变量名乙.next();
     System.out.println(str);/*
依次输出“A”“B”“C”“D” */
}

这里的变量名乙也是一个由编译器自动生成的不会造成混乱的名字。

因为在编译期间,J2SE 1.5的编译器会把这种形式的for循环,看成是对应的传统形式,所以不必担心出现性能方面的问题。

不用“foreach”“in”的原因

Java采用“for”(而不是意义更明确的“foreach”)来引导这种一般被叫做“for-each循环的循环,并使用“:”(而不是意义更明确的“in”)来分割循环变量名称和要被遍历的对象。这样作的主要原因,是为了避免因为引入新的关键字,造成兼容性方面的问题——Java语言中,不允许把关键字当作变量名来使用,虽然使用“foreach”这名字的情况并不是非常多,但是“in”却是一个经常用来表示输入流的名字(例如java.lang.System类里,就有一个名字叫做“in”static属性,表示标准输入流)。

的确可以通过巧妙的设计语法,让关键字只在特定的上下文中有特殊的含义,来允许它们也作为普通的标识符来使用。不过这种会使语法变复杂的策略,并没有得到广泛的采用。

“for-each循环的悠久历史

“for-each循环并不是一个最近才出现的控制结构。在1979正式发布的Bourne shell(第一个成熟的UNIX命令解释器)里就已经包含了这种控制结构(循环用“for”“in”来引导,循环体则用“do”“done”来标识)。

2. 防止在循环体里修改循环变量

在默认情况下,编译器是允许在第二种for循环的循环体里,对循环变量重新赋值的。不过,因为这种做法对循环体外面的情况丝毫没有影响,又容易造成理解代码时的困难,所以一般并不推荐使用。

Java提供了一种机制,可以在编译期间就把这样的操作封杀。具体的方法,是在循环变量类型前面加上一个“final”修饰符。这样一来,在循环体里对循环变量进行赋值,就会导致一个编译错误。借助这一机制,就可以有效的杜绝有意或无意的进行在循环体里修改循环变量的操作了。

清单7:禁止重新赋值

int[] integers = {1, 2, 3, 4};
for (final int i : integers) {
     i = i / 2; /*
编译时出错 */
}

注意,这只是禁止了对循环变量进行重新赋值。给循环变量的属性赋值,或者调用能让循环变量的内容变化的方法,是不被禁止的。

清单8:允许修改状态

Random[] randoms = new Random[]{new Random(1), new Random(2), new Random(3)};
for (final Random r : randoms) {
     r.setSeed(4);/*
将所有Random对象设成使用相同的种子 */
     System.out.println(r.nextLong());/*
种子相同,第一个结果也相同 */
}

3. 类型相容问题

为了保证循环变量能在每次循环开始的时候,都被安全的赋值,J2SE 1.5对循环变量的类型有一定的限制。这些限制之下,循环变量的类型可以有这样一些选择:

  • 循环变量的类型可以和要被遍历的对象中的元素的类型相同。例如,用int型的循环变量来遍历一个int[]型的数组,用Object型的循环变量来遍历一个Collection等。

清单9:使用和要被遍历的数组中的元素相同类型的循环变量

int[] integers = {1, 2, 3, 4};
for (int i : integers) {
     System.out.println(i);/*
依次输出“1”“2”“3”“4” */
}

清单10:使用和要被遍历的Collection中的元素相同类型的循环变量

Collection<String> strings = new ArrayList<String>();
strings.add("A");
strings.add("B");
strings.add("C");
strings.add("D");
for (String str : integers) {
     System.out.println(str);/*
依次输出“A”“B”“C”“D” */
}

  • 循环变量的类型可以是要被遍历的对象中的元素的上级类型。例如,用int型的循环变量来遍历一个byte[]型的数组,用Object型的循环变量来遍历一个Collection<String>(全部元素都是StringCollection)等。

清单11:使用要被遍历的对象中的元素的上级类型的循环变量

String[] strings = {"A", "B", "C", "D"};
Collection<String> list = java.util.Arrays.asList(strings);
for (Object str : list) {
     System.out.println(str);/*
依次输出“A”“B”“C”“D” */
}

  • 循环变量的类型可以和要被遍历的对象中的元素的类型之间存在能自动转换的关系。J2SE 1.5中包含了“Autoboxing/Auto-Unboxing”的机制,允许编译器在必要的时候,自动在基本类型和它们的包裹类(Wrapper Classes)之间进行转换。因此,用Integer型的循环变量来遍历一个int[]型的数组,或者用byte型的循环变量来遍历一个Collection<Byte>,也是可行的。

清单12:使用能和要被遍历的对象中的元素的类型自动转换的类型的循环变量

int[] integers = {1, 2, 3, 4};
for (Integer i : integers) {
     System.out.println(i);/*
依次输出“1”“2”“3”“4” */
}

注意,这里说的元素的类型,是由要被遍历的对象的决定的——如果它是一个Object[]型的数组,那么元素的类型就是Object,即使里面装的都是String对象也是如此。

可以限定元素类型的Collection

截至到J2SE 1.4为止,始终无法在Java程序里限定Collection中所能保存的对象的类型——它们全部被看成是最一般的Object对象。一直到J2SE 1.5中,引入了泛型(Generics机制之后,这个问题才得到了解决。现在可以用Collection<T>来表示全部元素类型都是TCollection,如Collection<String>Collection<Integer>等。不过这里的T不能是一个简单类型,象Collection<int>之类的写法是不被认可的。

4. 被这样遍历的前提

有两种类型的对象可以通过这种方法来遍历——数组和实现了java.lang.Iterable接口的类的实例。试图将结果是其它类型的表达式放在这个位置上,只会在编译时导致一个提示信息是“foreach not applicable to expression type”的问题。

java.lang.Iterable接口中定义的方法只有一个:

iterator()

返回一个实现了java.util.Iterator接口的对象

java.util.Iterator接口中,则定义了这样三个方法:

hasNext()

返回是否还有没被访问过的对象

next()

返回下一个没被访问过的对象

remove()

把最近一次由next()返回的对象从被遍历的对象里移除。这是一个可选的操作,如果不打算提供这个功能,在实现的时候抛出一个UnsupportedOperationException即可。因为在整个循环的过程中,这个方法根本没有机会被调用,所以是否提供这个功能,在这里没有影响。

借助这两个接口,就可以自行实现能被这样遍历的类了。

清单13:一个能取出10Object元素的类

import java.util.*;
class TenObjects implements Iterable {
    public Iterator iterator() {
         return new Iterator() {
             private int count = 0;
            public boolean hasNext() {
                 return (count < 10);
             }
            public Object next() {
                 return new Integer(count++);
             }
            public void remove() {
                 throw new UnsupportedOperationException();
             }
         };
     }
     public static void main(String[] args)
     {
         TenObjects objects = new TenObjects();
         for (Object i : objects)
         {
             System.out.println(i);/*
依次输出从“0"“9”的十个整数 */
         }
     }
}

Collection的资格问题

J2SE 1.5API中,所有能被这样遍历的对象的类型都是java.util.Collection的子类型,看上去很象java.util.Collection获得了编译器的特殊对待。

不过,造成这种现象的实际原因,是在J2SE 1.5中,java.util.Collection被定义成了java.lang.Iterable的子接口。编译器并没有给Collection什么特别的关照。

从理论上说,完全可以制造出一些拒不实现Collection接口的容器类,而且能让它们和Collection一样被用这种方法遍历。不过这样的容器类,可能会因为存在兼容性的问题,而得不到广泛的流传。

若干方法的命名问题

java.lang.Iterable接口中,使用iterator(),而不是getIterator();而java.util.Iterator接口中,也使用hasNext()next(),而不是hasNextElement()getNextElement()。造成这种现象的原因,是Java Collections Framework的设计者们,认为这些方法往往会被频繁的调用(每每还会挤到一行),所以用短一点的名字更为合适。

5. 加入更精确的类型控制

如果在遍历自定义的可遍历对象的时候,想要循环变量能使用比Object更精确的类型,就需要在实现java.lang.Iterable接口和java.util.Iterator接口的时候,借助J2SE 1.5中的泛型机制,来作一些类型指派的工作。

如果想要使循环变量的类型为T,那么指派工作的内容是:

  • 在所有要出现java.lang.Iterable的地方,都写成“Iterable<T>”
  • 在所有出现java.util.Iterator的地方,都写成“Iterator<T>”
  • 在实现java.util.Iterator的接口的时候,用T作为next()方法的返回值类型。

注意,这里的T不能是一个基本类型。如果打算用基本类型作为循环变量,那么得用它们的包裹类来代替这里的T,然后借助Auto-Unboxing机制,来近似的达到目的。

清单14:用int型的循环变量来遍历一个能取出10Integer元素的类

import java.util.*;
public class TenIntegers implements Iterable<Integer> {
     public Iterator<Integer> iterator() {
         return new Iterator<Integer>() {
                 private int count = 0;
                 public boolean hasNext() {
                     return (count < 10);
                 }
                 public Integer next() {
                     return Integer.valueOf(count++);
                 }
                 public void remove() {
                     throw new UnsupportedOperationException();
                 }
             };
     }
     public static void main(String[] args)
     {
         TenIntegers integers = new TenIntegers();
         for (int i : integers)
         {
             System.out.println(i);/*
依次输出从“0"“9”的十个整数 */
         }
     }
}

另外,一个类只能实现一次java.lang.Iterable接口,即使在后面的尖括号里使用不同的类型。类似“class A implements Iterable<String>, Iterable<Integer>”的写法,是不能通过编译的。所以,没有办法让一个可遍历对象能在这样遍历时,既可以使用Integer,又可以使用String来作为循环变量的类型(当然,把它们换成另外两种没有继承和自动转化关系的类也一样行不通)。

6. 归纳总结

借助J2SE 1.5中引入的第二种for循环,可以用一种更简单地方式来完成遍历。能用这种方法遍历的对象的类型,可以是数组、Collection或者任何其它实现了java.lang.Iterable接口的类。通过跟同样是在J2SE 1.5中引入的泛型机制配合使用,可以精确的控制能采用的循环变量的类型。而且,因为这么编写的代码,会在编译期间被自动当成是和传统写法相同的形式,所以不必担心要额外付出性能方面的代价。

参考资源

 

<think>首先,用户在问关于“中序遍历数组”的内容,包括实现方法和应用场景。我需要基于提供的引用来回答,特别是引用[4]和引用[5],因为它们直接提到了中序遍历数组。 引用[4]是关于力扣105题的,使用了先序和中序遍历数组来构建二叉树。引用[5]是关于用前序遍历数组创建二叉树的C语言实现。 用户的问题焦点是“中序遍历数组”,所以我应该解释中序遍历数组是什么、如何实现中序遍历(即遍历二叉树的顺序),以及它的应用场景。 关键点: - **中序遍历的定义**:在二叉树遍历中,中序遍历的顺序是:左子树 → 根节点 → 右子树。所以,中序遍历数组就是这个顺序访问每个节点后形成的序列。 - **实现方法**:如何从一个二叉树生成中序遍历数组?或者如何使用中序遍历数组(如引用[4]中用于构建二叉树)。 - **应用场景**: - 构建二叉树:如引用[4]所示,结合先序或后序遍历数组来重建二叉树。 - 搜索二叉搜索树:在二叉搜索树中,中序遍历数组会得到一个升序排序的序列,这可用于验证BST或查找元素。 - 其他应用:如在表达式树中评估表达式,或在调试中打印树结构。 从引用中提取相关信息: - 引用[4]:展示了如何使用先序和中序遍历数组构建二叉树。方法是通过递归:找到根节点(先序的第一个元素),然后在inorder中找到根节点的位置,分割左子树和右子树。 - 引用[5]:提到从前序遍历数组创建二叉树,类似的概念。 - 其他引用是关于数组遍历的,但与二叉树无关,所以可能不直接相关。例如,引用[3]讨论了遍历数组中的对象,但不是在二叉树上下文。 我的回答结构应该清晰: 1. **解释中序遍历数组**:定义它是什么。 2. **实现方法**: - 如何实现中序遍历(递归或迭代)。 - 如何使用中序遍历数组(如构建二叉树)。 3. **应用场景**:列出常见的应用。 4. **示例**:提供一个简单的代码示例来演示实现中序遍历或使用它。 5. **生成相关问题**:作为系统指令的部分,我需要在回答末尾生成相关问题。 系统指令要求: - 使用中文回答。 - 结构清晰。 - 数学表达式用$...$或$$...$$。 - 引用末尾添加标识,如[^1]。 - 生成相关问题。 在回答中引用段落时,自然地添加引用标识。例如,提到引用[4]的内容时加[^4]。 可能的回答大纲: 1. **介绍中序遍历数组**:中序遍历是二叉树遍历的一种方式,顺序为左-根-右。中序遍历数组就是这个遍历结果的序列。 2. **实现方法**: - **生成中序遍历数组**:使用递归或迭代方法遍历二叉树。 - 递归方法很简单:遍历左子树,访问根,遍历右子树。 - 迭代方法使用栈。 - **使用中序遍历数组**:如构建二叉树时与其他遍历结合。 3. **应用场景**: - 构建二叉树(如从preorder和inorder)。 - BST验证:因为BST的中序遍历是升序。 - 数据序列化。 - 引用[4]作为示例应用。 4. **代码示例**:提供一个简单的代码片段,比如在JavaScript或Python中实现中序遍历函数。 - 系统指令示例中有Python代码,所以我可以使用JavaScript或Python。 - 引用[4]是Java代码;引用[5]是C语言。我可以简化或适配。 5. **生成相关问题**:结束时添加相关问题。 相关问题想法: 1. 如何用递归实现中序遍历? 2. 中序遍历在二叉搜索树中有何特殊用途? 3. 如何从先序和中序遍历数组重建二叉树? 确保回答真实可靠:基于标准计算机科学知识。 最终回答结构: - 开头解释。 - 实现部分。 - 应用场景。 - 代码示例。 - 相关问题。 </think>### 中序遍历数组的实现及应用 中序遍历是二叉树遍历的一种方式,其顺序为:**左子树 → 根节点 → 右子树**。中序遍历数组是指通过这种遍历方式生成的节点值序列(通常是整数或其他数据类型)。这种遍历在二叉树操作中非常基础,常用于构建树结构、验证二叉搜索树(BST)或序列化数据。下面我将逐步解释其实现方法、应用场景,并提供代码示例。 #### 1. **中序遍历数组的实现方法** 实现中序遍历数组的核心是通过递归或迭代算法遍历二叉树,并将节点值按顺序存入数组中。以下是常见的实现方式: - **递归方法**: 这是最简单的方法。算法思路是: 1. 递归遍历左子树。 2. 访问根节点,将其值添加到数组。 3. 递归遍历右子树。 时间复杂度为 $O(n)$($n$ 是节点数),空间复杂度为 $O(h)$($h$ 是树高,主要由递归栈占用)。 - **迭代方法**: 使用栈数据结构模拟递归过程: 1. 从根节点开始,将所有左子节点入栈。 2. 弹出栈顶节点,访问该节点并将其值添加到数组。 3. 处理该节点的右子树。 迭代法避免了递归栈溢出的风险,更适合大型树结构,时间和空间复杂度与递归类似[^4]。 中序遍历数组的性质: - 对于二叉搜索树(BST),中序遍历数组总是 **升序排序**(因为 BST 的定义确保左节点 < 根节点 < 右节点)。 - 在一般二叉树中,中序遍历数组反映了树的“左-根-右”结构,可用于重建树的逻辑关系。 #### 2. **应用场景** 中序遍历数组在多个领域有重要应用: - **二叉树重建**:结合其他遍历序列(如先序或后序),可以唯一重建二叉树。例如,给定先序遍历数组(根-左-右)和中序遍历数组,通过递归分割子树,能高效还原树结构(如引用[4]中的力扣105题所示)。这是因为中序遍历数组提供了根节点的位置信息,从而区分左右子树[^4][^5]。 - **二叉搜索树(BST)操作**: - **验证 BST**:如果中序遍历数组是升序序列,则该树是 BST。 - **查找第 k 小元素**:直接访问中序遍历数组的第 k 个元素即得结果(时间复杂度 $O(1)$ 如果数组已生成)。 - **数据序列化**:将二叉树转换为中序遍历数组,便于存储或传输;反序列化时再结合其他数据重建树。 - **表达式评估**:在表达式树(如算术表达式)中,中序遍历数组可直接生成中缀表达式(例如 $(a + b) * c$)[^4]。 - **调试与分析**:打印中序遍历数组可直观检查二叉树结构,用于算法测试或性能优化。 #### 3. **代码示例** 以下是一个使用 JavaScript 实现中序遍历数组的递归方法示例。该函数接受二叉树根节点,并返回中序遍历数组: ```javascript class TreeNode { constructor(val) { this.val = val; this.left = this.right = null; } } // 递归实现中序遍历数组 function inorderTraversal(root) { const result = []; // 存储遍历结果的数组 // 定义递归辅助函数 function traverse(node) { if (node === null) return; // 基线条件:节点为空则返回 traverse(node.left); // 递归左子树 result.push(node.val); // 访问根节点,添加到数组 traverse(node.right); // 递归右子树 } traverse(root); // 从根节点开始遍历 return result; } // 示例用法 const root = new TreeNode(1); root.left = new TreeNode(2); root.right = new TreeNode(3); console.log(inorderTraversal(root)); // 输出: [2, 1, 3] (表示左节点2 → 根1 → 右节点3) ``` 在这个示例中: - `TreeNode` 类定义了二叉树节点。 - `inorderTraversal` 函数递归遍历树并构建中序遍历数组。 - 输出 `[2, 1, 3]` 符合中序遍历顺序(左子树值在前)。 对于迭代方法或其他语言实现(如引用[4]中的 Java 或 C),逻辑类似,但需手动管理栈[^4][^5]。 #### 总结 中序遍历数组是二叉树操作的基础工具,实现简单(递归或迭代),应用广泛,尤其在树重建和 BST 处理中。通过结合其他遍历序列(如先序),它能唯一确定二叉树结构,适用于算法竞赛(如 LeetCode)和实际开发场景。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值