课时30:数组转置案例分析
摘要:本次分享的主题是数组转置案例分析。主要分为二个部分:
1.数组反转的具体操作
2.内存分析
01.数组反转的具体操作
接下来看一下数组反转的操作。什么叫数组反转?数组反转操作指的是将数组中的元素进行前后转置处理,也就是说将数组的首尾元素进行交换。假设现在有一个数组,其内容如下: 123456789 。那么要求交换后的内容是什么?是 987654321 。
那么数组反转如何进行?对于数组的前后交换有两种方法,先分别看一下它们各自的特点,比如现在来看第一种方法,先定义一个新的数组,然后按照逆序的方式保存原数组的元素。那么按照逆序方式保存是什么意思?简单来说就是从原数组的最后一个元素开始,依次将元素放入新数组中。
比如现在要讨论的是数组转置。
这个过程是这样的:先有一个原始数组,内容依次是 1 、 2 、 3 、 4 、 5 、 6 、 7 、 8 、 9 。注意现在没有数字 10 。那么现在要求的是这个数组的转置结果。转置的过程就是根据现有的数组元素,重新排列它们的位置。那么这里的角标是从几开始计算的?
那么按照常规的开发过程,角标的顺序应该是怎样的?应该是升序排列。通常这种升序结构在排序时会从 0 开始一直排列到某个特定的数字。不过这里可能有些混淆,正确的说法应该是从 0 排列到8 升序排列。
那么如果按照这样的规则来排列,整个过程中的角标会告诉我们什么?首先从 0 开始设置第一个数据的位置,这是获取数据时的情况。但如果是在设置数据时?假设在这里预先设定了一个值,对应角标为 8 ,这个值暂时不变。接下来依次有 1 、 2 、 3 、 4 、 5 、 6 ,然后再加上 7 ,接着是 8 ,最后是 9 。
那么从角标 8开始应该按照什么样的顺序进行后续操作?按照这一边的顺序进行,也就是说采用的是降序排列,即从 8 开始,然后是 7 、 6 、 5 ……。这样,从 0 到 8 的序列在降序排列下就变成了 8 、 7 、 6 ……的顺序。这是现在所说的方法,即生成一个新的数组,并通过逆序的模式来完成数组的转置。那么按照这样的方法来实现。接下来编写相应的代码。
在这个过程中先简化一下程序,去掉不必要的部分,只保留输出方法,以便清晰地展示结果。、现在有一个数组,里面存放的是 1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 , 9 这些数字,接下来编写代码。首先定义一个名为 Temp 的第二个数组,其长度应该与第一个数组相同。然后定义一个变量 Int Fot ,并将其初始化为 T
emp.length - 1
。那么这里描述的 Fot 是第二个数组的索引下标。
System.out.print (temp[x]+"、"); } System.out.println() ; } } public class ArrayDemo { public static void main(String args[]) { int data [] = new int[]{1,2,3,4,5, 6, 7,8,9} ; ArrayUtil.printArray (data); } }
接下来继续完善代码并解释其含义。首先编写一个 For 循环,初始化 I
nt x = 0
,条件是 X < Date.length
,每次循环 X++
。在循环体内为 Temp 数组赋值, Temp[date.length - 1 - x] = Date[x]
。这里 Date 是原始数组,通过逆序的方式将 Date 数组的元素赋值给 Temp 数组。
保存完上述代码后实现了 Temp 数组是Date 数组的逆序版本。但目标通常是得到一个可以直接使用的新数组,所以接下来将 Temp 数组的值赋给一个新的数组。现在来看一下最终的结果,通过编译和执行代码能否得到逆序的数组。现在已经通过这段代码实现了所需的功能。但是还需要进一步分析内存的使用情况。
System.out.print(temp[x] +"、") ;} } System.out,println() ; } } public class ArrayDemo { public static void main(String args[]) { int data [] = new int []{1,2,3,4, 5, 6,7,8,9}; int temp [] = new int [data.length] ; //第二个数组 int foot = temp.length - 1; //第二个数组的脚标 for (int x =0 ; x<data.length ; x ++){ temp[foot --] = data[x] ; } data = temp ; ArrayUtil.printArray (data);} } }
02.内存分析
这个程序该如何操作?关于数组转置处理方式一,由于数据量较大不会像刚才那样展示全部数据,只象征性地展示几个数字就可以了。首先这段代码的第一步是定义一个新的 Date 数组,这一点大家应该都已经知道了,即需要用它来保存相应的内容。
这里先说明一下不会全部保存,只是象征性地保存几个数字,比如 1 、 2 、 3 。紧接着在这段代码中,第二步是声明一个名为 Int Temp 的变量,并为其分配一个新的整型空间。
这句话的含义无需赘述,接下来打算创建一个新数组。当然如果空间不够就需要删除一些内容,新数组的长度将与原始数组相同,但其内容将全部设置为其对应数据类型的默认值。这里有一个变量叫做 Temper ,现在将在这一步骤中对其进行处理。
当然最关键的问题是现在要进行的是重新赋值操作,也就是为 Temper 赋值。而且这个过程是否会逆序处理 Date 数组?不论是否逆序处理只需关注一点:如果这段代码真正执行完毕后, Temper 的值应该变为321 。
而最终的关键性问题在于此: Date 等于 Temper 。这句话究竟意味着什么?无需进一步详细描述或图示,因为已经可以明确这是关于数组的操作。关于数组的具体内容甚至无需绘制得过于详尽。这就是数组,其内容如何不必过分深究。
在程序设计中一个最令人头疼但也是至关重要的问题是:希望整个代码中,原本说 Data 有数组内容, Temple 也有数组内容。但由于这样一句话的存在整个代码的结构就会出现一个特别不理想的状态。这个不理想的状态就是它会告诉你现在的 Date 不再需要了,要断开与它的连接。原本由 Data 指向的内容会转移到哪里?它会指向新的操作对象。
现在可以看到的确实现了一个数组逆序的操作,但这个逆序是以什么为代价?是以产生垃圾数据为代价的。那么这样的逆序显然是不理想的。因此第一种做法会产生垃圾。回顾第一种做法会发现它会导致一些无用的内存空间被占用。即每一个内存空间都应该被极致利用,所以现在来尝试第二种做法。
先提出一个可以在一个数组上进行转置操作。当然有些同学可能会担心,在数字数组上进行转置是否会遇到奇偶数的问题?为了说明问题这里不打算使用太多的数据,因为数据过多会使得描述变得复杂。接下来将采用两个具有代表性的数据组来进行说明。首先这是第一组数据,为了简化会稍微减少一些数据。那么这组数据的个数是奇数,现在正在准备一组奇数数据。
第二个数被称之为 6 ,它是一个偶数,现在如果要对奇数个数进行转置,最佳的方法是怎样的?第一次转置是谁跟谁交换?是 5 和1 交换,然后再进行第二次转置,是 4 和 2 交换。转置完成后整个流程已经实现了转置的处理操作。那么如果是以它为主进行转置,现在来看偶数个数的情况。
首先进行第一次转置,是 1 与 6 进行交换。接着进行第二次转置,是 5 与 2 进行交换。然后再进行第三次转置,是 4 与 3 进行交换。通过这一系列的比较和交换之后大概就能知道哪些数需要交换,哪些数不需要交换了。
对于这些换与不换的操作需要进行一番细致的讨论。先把这个大碗放在这里,之后用一条虚线来表示,并给它一个稍微浅一点的颜色以示区分。
现在把这个虚线放在中间位置。当明确了整个转置过程之后这个交换的次数是如何计算出来的?然而中间的部分其实并没有进行太多的交换,因此会自然而然地认为现在要实现这种转置,最关键的就是要确定数组转换的次数。
那么次数的计算应该是基于什么?是基于数组长度除以 2 。但这个时候是否需要考虑数组是奇数个元素还是偶数个元素?实际上并不需要去考虑数组是奇数个元素还是偶数个元素。
接下来以此为例来回答问题。 5 除以 2 等于 2.5 ,但由于整数不保留小数位,所以实际进行 2 次交换。那么 6 除以 2 等于 3 ,则进行 3 次交换,正好满足要求。所以现在已经清楚了这个代码应该怎么写。
并且之前的写法不好,应该做出改变。那么代码可以这样写:首先定义一个变量 Center ,它等于 Date.length 除以 2的结果。注意这里应取整数结果,即向下取整,以确定转换的次数。
System.out.print(temp[x] +"、"); } System.out.println() ; } } public class ArrayDemo { public static void main(String args[]){ int data [] = new int [] {1,2,3, 4,5, 6, 7,8,9} ; ArrayUtil.printArray (data); } }
完成上述步骤后现在正处于下方,并定义了一个名为 Int 的变量,注意这里 Int 有两个下标,一个表示递增,一个表示递减。另外还定义了一个名为 Head 的变量,其初始值为 0 ,这个变量用于标记操作的下标。同时还定义了一个名为 Tell 的变量,其值等于Data.lse 减 1 ,这也是一个用于操作的下标。
接下来进入了一个 For 循环,其中 Int x = 0
,循环条件是 X
<
C
enter
,每次循环 X++ 。在这个循环内部执行了一个数据交换操作:首先定义了一个临时变量 Temper ,并将其赋值为 Data 。然后找到 Head 所指向的元素,将 Data 中 Head 位置的值与 Data中 Tell 位置的值进行交换。
具体来说就是将 Data[head]
赋值为Data[tell]
,再将 Data[tell]
赋值为 Temper 的值,从而完成了交换。那么在交换完成后 Head 是否需要递增?是的,这通常是拣选操作的一部分。
} } public class ArrayDemo{ public static void main(String args[]) { int data [] = new int[] {1,2,3,4,5,6,7,8,9}; int center = data.length /2 ; // 确定转换的次数 int head =0; // 操作脚标 int tail =data.length-1;//操作脚标 for (intx=0;x<center ;x++){ int temp = data [head]; data [head] } ArrayUtil.printArray (data); } }
再次审视处理完成后的结果以确认其是否满足要求。之后再次进行编译并执行程序,并特别关注转置操作的结果。这里的转置是在原地实现的,也就是说它不需要额外的存储空间来存储转置后的矩阵,而是在原矩阵上进行操作。
就程序而言,如果对比两个程序版本,可以发现第一个程序的循环次数要多于第二个程序。当对两种实现方式进行比较时,可以发现第一种处理方式的循环次数较多,并且可能会导致额外的内存开销。而第二种实现方式的循环次数有所降低。
然而在第二种实现中引入了 If 判断语句,这在一定程度上增加了时间复杂度。但也需要考虑到第二种实现方式可以减少无用对象的产生,这对于提升程序性能是有帮助的。因此综合考量两种方式,第二种实现方式在整体上来看会更为合适。
} ArrayUtil.printArray (data); } }
当然为了确保代码能够正常编写还需要完成一个重要的步骤是应该将转换功能封装为一个受保护的方法。因为主方法中不应该包含过多的转换流程。所以可以在这里添加一个名为 Reverse 的方法。在未来的代码中,当需要进行转换时可以直接在 Reverse 方法中完成。这样在主方法中只需要调用 Obj.reverse() 就可以实现转置处理。这样的封装不仅提高了代码的可读性,还使得转换流程更加清晰和模块化。
接下来观察一下结果。先编译一下,然后再执行一次。会发现它们的效果是一样的。所以到这个阶段代码才算是一个较为合理的程序。这就是关于矩阵转置的过程。在处理数组时,由于可以通过索引来控制元素,因此循环逻辑的使用会比较频繁。无论是排序还是转置,往往都需要在循环中实现。这个过程实际上是科班出身所必须掌握的基本功。