日撸 Java 三百行(12 天: 顺序表(二))
注意:这里是JAVA自学与了解的同步笔记与记录,如有问题欢迎指正说明
· 前言
今天的代码内容是对昨日顺序表对象的方法扩充,针对昨日关于类的补充以及顺序表的一些特性和基础方法可见第 11 天: 顺序表(一)
今天我们的任务是对于顺序表方法的基础扩充,因此就继续沿用昨日的的类,补充几个关键的函数和改写测试用的main函数即可。
一、顺序查找
顺序查找的含义是:给出任意一个数,在顺序表中查找此元素首次出现时的下标号。
默认的顺序查找方法就是从头到尾遍历数据(什么方向都行),一但发现合适的数据就返回下标即可,如果结束遍历后都没有发现合适元素就返回一个定义内的下标非法的数值(比如-1)。
很自然推理出这样的复杂度是O(n),这是一种很自然的策略,也是正常人直接能想到的。另外,顺序查找有一些衍生的方法,比如针对有序表,可以利用二分查找来提高查找的效率到O(logn),或者更高阶你可以用索引顺序查找改善查找效率。
对于有n个元素的表,若采用逆序比较,给定值Key与表中的第i个元素相等,即定位第i个元素时,需进行n-i+1次关键字比较,即Ci=n-i+1。查找成功时,顺序查找的平均长度(Average Search Length)为:
当每个元素的查找概率相等时,Pi=1/n,有
代码如下:
/**
*********************
* Find the index of the given value. If it appears in multiple positions,
* simply return the first one.
*
* @param paraValue The given value.
* @return The position. -1 for not found.
*********************
*/
public int indexOf(int paraValue) {
int tempPosition = -1;
for (int i = 0; i < length; i++) {
if (data[i] == paraValue) {
tempPosition = i;
break;
} // Of if
} // Of for
return tempPosition;
}// Of indexOf
补充:关于顺序查找方案中的“哨兵”思想
另外一种顺序查找的方案是设置一个正常遍历顺序之外的元素,例如,如果我们所有元素都数据存于下标于[1~N]的位置时,可以在[0]下标预先存入我们要查找的元素,然后从尾到头进行遍历,若遍历结束时返回的下标在[1~N]内那么就是正确的位置,若是[0],那么自然就出错了。这种填充法是灵活的,例如一般编程的[1~N-1]范围元素可以把违规区设置在[N]。这个方法叫做哨兵标记法。
这种方法好不好呢?
辩证来看,"哨兵"的引入确实可以令数组不必判断越界问题,到头时循环自己会退出,而且这种"哨兵"思想确实也能避免很多不必要判断(就不需要诸如i<length这种判断了)。
但是另一方面来看,其需要牺牲一位来存储,实际代码中我们常用[1~N-1]的范围内的数据,如果要扩充到[N]反而可能会徒增麻烦。这种哨兵判断对于某些习惯仅用[1~N]来使用而[0]闲置不用的建表方案也许划算,但是,对于常见的[0~N-1]的建表方案来说,优化收益可能还不及再添加一个元素的麻烦。
二、顺序表的插入操作
线性表最喜欢用的方法无非就是增删改查这些操作,其中插入和删除是体现顺序表思维比较重要的一些操作,也是往往复杂度最高的一些操作。
在进行插入之前要实现进行两个判断:
1.顺序表是否满了?
2.插入的位置合理吗?
这两个基本问题的解决是顺序表健壮性的基本保障。
插入操作可以用一个图来说明:
从上图的下标为[length]的位置开始遍历。
(length也就是以数组长度的值为下标,直观来看是我们数组的最后一个元素的下一个位置,这个位置是未被使用的,若要访问必须保证这个位置是可以访问的,所以最开始我们需要判断当前数组是否已经满了)
然后从尾到头遍历至paraPosition,也就是我们需要插入的位置。
遍历过程中:data[i] = data[i - 1];
也就是说不断用 前一个元素 覆盖当前元素,这也就是为什么我们从后向前遍历的原因,因为正向遍历是无法实现前段向后段覆盖的。
完成本次遍历之后,我们目标位置的元素之后的全部区域都已经实现一次后移的拷贝,这样的话,就构成了 “目标元素的后方又插入了相同目标元素” 的事实,然后只需要在图中的原paraValue位置修改为我们的插入元素即可实现前插入。自然,若在paraValue+1位置修改就可以实现后插。
代码如下:
public boolean insert(int paraPosition, int paraValue) {
if (length == MAX_LENGTH) {
System.out.println("List full.");
return false;
} // Of if
if ((paraPosition < 0) || (paraPosition > length)) {
System.out.println("The position " + paraPosition + " is out of bound.");
return false;
}
// From tail to head. The last one is moved to a new position.
// Because length < MAX_LENGTH, no exceeding occurs.
for (int i = length; i > paraPosition; i--) {
data[i] = data[i - 1];
} // Of for i
data[paraPosition] = paraValue;
length++;
return true;
}// Of insert
三、顺序表的删除操作
自然,我们再来看删除操作。删除操作与插入操作都沿用了类似的 “段的整体覆盖” 效果,也因此,在我们常常在谈论插入时都会附带讨论下删除。
同样,在进行插入之前要实现进行两个判断:
1.顺序表是否空了?
2.插入的位置合理吗?
但是在实际应用时,顺序表如果是空的话,长度是0,那么在数据非法边界判断的时候((paraPosition < 0) || (paraPosition >= length)
),判断的范围成为一切实数,无论什么边界都可判断为非法。因此,我们只需要判断删除的位置合理即可。
插入操作可以用一个图来说明:
从上图的下标为[paraPosition]的位置开始遍历,遍历过程中:data[i] = data[i + 1];
也就是说,不断用 后一个元素 覆盖当前元素,这也就是为什么我们从前向后遍历的原因,因为逆向遍历是无法实现后段向前段覆盖的。
每次遍历时都令i的后方元素覆盖当前元素,直至遍历到length - 2(i < length-1
),也就是最后一次执行data[i] = data[i + 1];
时,data[i+1]元素恰好是最后一个元素。
完成本次遍历之后,直观来看我们原paraPosition的元素被后端的全部内容覆盖了,这样就实现了删除。
当然,操作完成,末尾元素出现了重复,因此需要我们逻辑上对其进行删除(令length-1)
代码如下:
/**
*********************
* Delete a value at a position.
*
* @param paraPosition The given position.
* @return Success or not.
*********************
*/
public boolean delete(int paraPosition) {
if ((paraPosition < 0) || (paraPosition >= length)) {
System.out.println("The position " + paraPosition + " is out of bounds.");
return false;
} // Of if
// From head to tail
for (int i = paraPosition; i < length - 1; i++) {
data[i] = data[i + 1];
} // Of for i
length--;
return true;
}// Of delete
四、过程模拟
代码如下:
/**
*********************
* The entrance of the program.
*
* @param args Not used now.
*********************
*/
public static void main(String args[]) {
int[] tempArray = { 1, 4, 5, 9 };
SequentialList tempFirstList = new SequentialList(tempArray);
System.out.println("After initialization, the list is: " + tempFirstList.toString());
System.out.println("Again, the list is: " + tempFirstList);
int tempValue = 4;
int tempPosition = tempFirstList.indexOf(tempValue);
System.out.println("The position of " + tempValue + " is " + tempPosition);
tempValue = 5;
tempPosition = tempFirstList.indexOf(tempValue);
System.out.println("The position of " + tempValue + " is " + tempPosition);
tempPosition = 2;
tempValue = 5;
tempFirstList.insert(tempPosition, tempValue);
System.out.println("After inserting " + tempValue + " to position " + ", the list is: " + tempFirstList);
tempPosition = 8;
tempValue = 10;
tempFirstList.insert(tempPosition, tempValue);
System.out.println(
"After inserting " + tempValue + " to position " + tempPosition + ", the list is: " + tempFirstList);
for (int i = 0; i < 8; i++) {
tempFirstList.insert(i, i);
System.out.println("After inserting " + i + " to position " + i + ", the list is: " + tempFirstList);
} // Of for i
tempFirstList.reset();
System.out.println("After reset, the list is: " + tempFirstList);
}// Of main
基本说明下,首先初始了一个包含元素1、4、5、9的顺序表,输出必要答应,然后分别进行了下述操作:
1.查找数字4、5在表中的位置,并打印信息
2.在下标为2的元素前插入5,在下标为8的元素前插入10(这个越界了)
3.删除下标为3的元素
4.在下标为i的元素前插入元素i (0 < i < 8) (这个在插入过程中会导致顺序表满,后续元素将无法插入)
运行展示如下
总结
今天主要实现了如上的三个操作,这三个操作的复杂度是O(n),体现了顺序表的主要操作。
这其中使用的 “成段覆盖元素” 的思想与我们现实中的删除与添加操作似乎颇有些有些不同,因为这里其实并没有很明显的 “插入新物” 与 “删除旧物” 的既视感。整个操作中我们的空间并没有额外添加和减少,对于删除的内容用的是 “覆盖” 而非真正意义上的 “清除” 。
我感觉,这应该是因为顺序表在存储结构上必须要求元素各自紧密排在一起,所以其不可能真的做到在两个地址空间中突然冒出一个新的地址空间。地址空间在硬件出厂时顺序就确定了,无论逻辑层面怎么表现都不可能做到真正的删除与添加。
要硬说,顺序表的操作在存储结构层面,更像是电影院的固定的座位,其位置是定死了的,而人就是逻辑上的元素,如果一群人有序坐在一横排且中间无空位,这个时候如果中间有个男生的女朋友来了,想坐在这个男生的旁边,那必然她会一个个去麻烦这个男生某个方位的全部人,麻烦他们同向个方向挪下屁股,好保证这个男生旁边能空出个位置。
当然这么说其实也有些细节漏洞啦,因为“空位”的概念我这模糊说明了。
但无论怎么说,我的本意就是要提醒一点:要注意把握逻辑结构与物理结构之间的度。我们谈论一个算法时,其逻辑结构更多是完成算法的设计,而实际的物理结构是完成算法的实现,因此在算法的实现过程中,我们必须时刻警惕计算机物理结构的特性,因为这种特性可能时时刻刻都会与我们的常规逻辑思维作对。