任务1
在本次实验中,主要完成的任务是:
1、为指定的 List ADT(该 ADT 的具体要求请参见文件 List.java)实现三种数据结构:①使用顺序数组做为存储表示; ②使用单向链表做为存储表示; ③使用双向链表做为存储表示。不论哪种存储表示下实现的数据结构,实验中需要处理的数据类型都是 Character 类型。
2、用给定的输入数据文件验证所实现的数据结构是否正确。
3、使用表格列举每种数据结构下所实现的所有方法的时间复杂度。
为了方便进行测试验证,对 List 的各种操作指定了相应的命令符号,具体的符号含义如下:
实验完成之后,必须通过实验中提供的测试用例。借助测试用例的运行结果,用来检查所撰写的代码功能是否正确。
测试用例中的每一行的内容都类似于上表中的每一行“命令行内容”列中所指示的内容。要求每执行一行,就调用 List 接口中的 showStructure 行为,用以验证该命令行的执行是否正确。
每行“命令行内容”都不是独立的,是针对同一个 List 类型的对象实例运行的结果。实验包里包括了个文件,一个是“list_testcase.txt”,其内包含了测试用例;另一个是“list_result.txt”,其内包含了对应测试用例的运行结果。
实验要求三个数据结构分别是数组、单向链表、双向链表,三者都实现了List<T>, Iterable<T>。
数组Array包括private T[] storage表示储存的数组;private int capacity表示容量;private int count表示元素数量;private int curr表示当前元素的位置。
Capacity等于题目要求512,curr最开始指向-1,为了方便遍历实现了抽象方法public Iterator<T> iterator()。
- public class Array<T> implements List<T>, Iterable<T> {
- private T[] storage;//储存的数组
- private int capacity;//容量
- private int count;//元素数量
- private int curr;//当前元素的位置
- //不带初始容量的构造方法。默认容量为512
- public Array() {
- this.capacity = 512;
- this.curr = -1;
- this.storage = (T[]) new Object[512];
- this.count = 0;
- }
- @Override
- public Iterator<T> iterator() {
- curr = 0;
- return new Iterator<T>() {
- @Override
- public boolean hasNext() {
- return curr < count;
- }
- @Override
- public T next() {
- T value = storage[curr];
- curr++;
- return value;
- }
- };
- }
- }
单向链表SingleLinkedListSentinel<T>包括内部类Node<T>,Node<T>有T value和Node<T> next;还有两个哨兵节点private Node<T> head和private Node<T> tail以及指针private Node<T> p。
单向链表节点head指向节点tail,指针p指向head,为了方便遍历实现了抽象方法public Iterator<T> iterator()。
- public class SingleLinkedListSentinel<T> implements List<T>, Iterable<T> {
- private static class Node<T> {
- T value;
- Node<T> next;
- public Node(T value, Node<T> next) {
- this.value = value;
- this.next = next;
- }
- }
- //两个哨兵节点
- private Node<T> head;
- private Node<T> tail;
- private Node<T> p;//指针
- public SingleLinkedListSentinel() {
- head = new Node<T>(null, null);
- tail = new Node<T>(null, null);
- head.next = tail;
- p = head;
- }
- @Override
- public Iterator<T> iterator() {
- return new Iterator<T>() {
- Node<T> p = head.next;
- @Override
- public boolean hasNext() {
- return p != tail;
- }
- @Override
- public T next() {
- T value = p.value;
- p = p.next;
- return value;
- }
- };
- }
- }
双向链表DoubleLinkedListSentinel<T>包括内部类Node<T>,Node<T>有T value、Node<T> next和Node<T> prev;还有两个哨兵节点private Node<T> head和private Node<T> tail以及指针private Node<T> p。
双向链表节点head的next指向节点tail,tail的prev指向head,指针p指向head,为了方便遍历实现了抽象方法public Iterator<T> iterator()。
- public class DoubleLinkedListSentinel<T> implements List<T>, Iterable<T> {
- private static class Node<T> {
- T value;
- Node<T> next;
- Node<T> prev;
- public Node(T value, Node<T> next, Node<T> prev) {
- this.value = value;
- this.next = next;
- this.prev = prev;
- }
- }
- //两个哨兵节点
- private Node<T> head;
- private Node<T> tail;
- private Node<T> p;//指针
- public DoubleLinkedListSentinel() {
- head = new Node<T>(null, null, null);
- tail = new Node<T>(null, null, null);
- head.next = tail;
- tail.prev = head;
- p = head;
- }
- @Override
- public Iterator<T> iterator() {
- return new Iterator<T>() {
- Node<T> p = head.next;
- @Override
- public boolean hasNext() {
- return p != tail;
- }
- @Override
- public T next() {
- T value = p.value;
- p = p.next;
- return value;
- }
- };
- }
- }
当实现了三种数据结构后,剩下比较难办的就是怎么用StreamTokenizer实现命令行。
注意到每一行都会生成一个{capacity = 16, length = 13, cursor = 1},也就是每一行调用一次showStructure(writer)。我们可以把根据行读入,然后当有下一行时做相应的操作。
- String line;
- while ((line = br.readLine()) != null) {
- executeCommand(array, line);
- array.showStructure(writer);
- writer.flush(); // 刷新PrintWriter的缓冲区
- }
- writer.close(); // 在循环结束后关闭PrintWriter对象
为了方便起见,我们把读取标记并进行相对应的动作用方法抽离出来:
- private static void executeCommand(DynamicArray<Object> array, String line) throws ListException, IOException {
- StreamTokenizer st = new StreamTokenizer(new StringReader(line));
- int token = st.nextToken();
- while (token != StreamTokenizer.TT_EOF) {
- char command = (char) st.ttype;
- switch (command) {
- case '+' -> {
- st.nextToken();
- array.insert(st.sval);
- }
- case '-' -> array.remove();
- case '=' -> {
- st.nextToken();
- array.replace(st.sval);
- }
- case '#' -> array.gotoBeginning();
- case '*' -> array.gotoEnd();
- case '>' -> array.gotoNext();
- case '<' -> array.gotoPrev();
- case '~' -> array.clear();
- }
- token = st.nextToken();
- }
- }
当碰见+和=时,读取+或者=后面的符号并调用相对于的方法。读取到其他符号时,直接调用相对应的方法。
- Array
- /**
- * 在当前位置插入一个元素
- * 从curr开始的元素全部向后移动一位
- * curr指向被插入的元素
- */
- @Override
- public void insert(T newElement) throws ListException {
- //列表满了
- if (isFull()) {
- return;
- }
- if (curr < -1 || curr > capacity) {
- throw new ListException("插入位置不合适");
- }
- if(isEmpty()){
- curr = -1;
- }
- for (int i = count - 1; i > curr; i--) {
- storage[i + 1] = storage[i];
- }
- curr++;
- storage[curr] = newElement;
- count++;
- }
数组的insert实现起来很简单,需要注意的是各种特殊情况的处理。数组满了直接return;插入位置不合法则报错;数组为空时,把curr置为-1,因为下面要让curr++;随后将数组指针后面的所有元素依次向后移一位,再插入相应的数据即可。
如果插入位置合适,即curr在有效范围内(0到count-1之间),则需要将从curr开始的元素全部向后移动一位。这个操作的时间复杂度为O(n),其中n是从curr到count-1的元素个数。在移动元素后,将新元素插入到curr位置,并更新curr和count。这个操作的时间复杂度为O(1)。
综上所述,insert的时间复杂度是O(n)。
- /**
- * 从列表中移除游标标记的元素
- * 如果结果列表不为空,则将光标移动到被删除元素后面的元素
- * 如果被删除的元素位于列表末尾,则将光标移动到列表开头的元素
- */
- @Override
- public void remove() {
- //如果列表不为空就把后面的元素前移就行,如果已经空了就不用管了
- if (!isEmpty()) {
- for (int i = curr + 1; i < count; i++) {
- storage[i - 1] = storage[i];
- }
- count--;
- if (count == curr) {
- curr = 0;
- }
- }
- }
首先,检查列表是否为空。如果列表为空,即没有元素可移除,直接返回。这个操作的时间复杂度为 O(1)。如果列表不为空,需要将光标之后的元素向前移动一位。这个操作的时间复杂度取决于需要移动的元素个数,即从 curr + 1 到 count - 1 的元素个数,记为 n。在移动元素后,将列表中的元素个数减 1,并根据情况更新光标位置。这个操作的时间复杂度为 O(1)。
综上所述,remove 方法的时间复杂度为 O(n)。
- /**
- * 先决条件:List不为空,newElement也不为空。
- * 后置条件:用newElement替换游标标记的元素。游标保持在newElement处
- */
- @Override
- public void replace(T newElement) {
- if(isEmpty()){
- return;
- }
- storage[curr] = newElement;
- }
首先,检查列表是否为空。如果列表为空,即没有元素可替换,直接返回。这个操作的时间复杂度为 O(1)。如果列表不为空,则将游标标记的元素替换为新元素 newElement。这个操作的时间复杂度为 O(1),因为它只涉及一次元素的替换。
综上所述, replace 方法的时间复杂度为 O(1)。
- //删除列表中所有元素
- @Override
- public void clear() {
- Arrays.fill(storage, 0, count, null); // 将数组中的元素设为null
- count = curr = 0;//元素清空
- }
首先,通过调用 Arrays.fill() 方法将数组 storage 中从索引 0 到索引 count-1 的元素设为 null。这个操作的时间复杂度为 O(n),其中 n 是列表中的元素个数。接着,将列表中的元素个数 count 和游标位置 curr 都设为 0。这个操作的时间复杂度为 O(1)。
综上所述,整个 clear 方法的时间复杂度为 O(n)。
- @Override
- public boolean isEmpty() {
- return count == 0;
- }
- @Override
- public boolean isFull() {
- return count == capacity;
- }
isEmpty 和 isFull 方法的时间复杂度均为 O(1)。
- //如果列表不为空,则将光标移动到列表的开头并返回true。否则,返回false。
- @Override
- public boolean gotoBeginning() {
- if (!isEmpty()) {
- curr = 0;
- return true;
- } else {
- return false;
- }
- }
- @Override
- public boolean gotoEnd() {
- if (!isEmpty()) {
- curr = count - 1;
- return true;
- } else {
- return false;
- }
- }
- @Override
- public boolean gotoNext() {
- if (isEmpty() || curr == count - 1) {
- return false;
- }
- curr++;
- return true;
- }
- @Override
- public boolean gotoPrev() {
- if (isEmpty() || curr == 0) {
- return false;
- }
- curr--;
- return true;
- }
对于数组来说,这四个goto方法都是常数级的,这四个方法的时间复杂度均为 O(1)。
- /**
- * 前提条件: List不为空。
- * 返回游标所标记元素的副本。
- */
- @Override
- public T getCursor() {
- return storage[curr];
- }
GetCursor的时间复杂度也是 O(1)。
- /**
- * 输出列表中的元素和游标的值。
- * 如果列表为空,则输出“empty list”。
- * 注意,此操作仅用于测试/调试目的。
- */
- @Override
- public void showStructure(PrintWriter pw) {
- int current = curr;
- if (isEmpty()) {
- pw.print("Empty list");
- pw.print("{capacity = ");
- pw.print(capacity);
- pw.print(", length = ");
- pw.print(count);
- pw.print(", cursor = ");
- pw.print(-1);
- pw.println("}");
- } else {
- gotoBeginning();
- for (int i = 0; i < count; i++) {
- pw.print(storage[i] + " ");
- }
- curr = current;
- pw.print("{capacity = ");
- pw.print(capacity);
- pw.print(", length = ");
- pw.print(count);
- pw.print(", cursor = ");
- pw.print(curr);
- pw.println("}");
- }
- }
首先,检查列表是否为空。这个操作的时间复杂度为 O(1)。
如果列表为空,则输出固定的字符串和列表的一些属性值,这是一个常数时间操作。
如果列表不为空,则将光标 curr 移动到列表的开头。这个操作的时间复杂度为 O(1)。接下来,使用一个循环来遍历列表中的元素,并输出它们。由于遍历的次数取决于列表中的元素个数 n,因此这个操作的时间复杂度为 O(n)。最后,将光标 curr 恢复到原来的位置,输出列表的其他属性值,这都是常数时间操作。
综上所述,当array不为空时,整个 showStructure 方法的时间复杂度为 O(n),当array为空时,方法时间复杂度是O(1)。
- /**
- * 前提条件:列表至少包含n + 1个元素。
- * 从列表中删除游标标记的元素
- * 并将其作为列表中的第n个元素重新插入
- * 其中的元素从0开始从头到尾编号
- * 将光标移动到被移动的元素。
- */
- @Override
- public void moveToNth(int n) throws ListException {
- if (count < n + 1) {
- System.out.println("输入的n过大");
- return;
- }
- T removed = getCursor();
- remove();
- gotoN(n);
- insert(removed);
- }
首先,检查列表中的元素个数是否至少为 n + 1。如果不满足该条件,输出错误信息并返回。这个操作的时间复杂度为 O(1)。
如果满足条件,首先获取当前游标标记的元素 removed,这是一个常数时间操作。调用 remove 方法删除当前元素,这个操作的时间复杂度为 O(n),其中 n 是当前游标之后的元素个数。调用 gotoN 方法将光标移动到第 n 个位置,这个操作的时间复杂度为 O(n)。
调用 insert 方法将之前删除的元素 removed 插入到列表中,这个操作的时间复杂度为 O(n)。
综上所述,整个 moveToNth 方法的时间复杂度为 O(n)。
- /**
- * 前提条件: List不为空。
- * 在列表中搜索searchElement。
- * 从游标标记的元素开始搜索。在列表中移动光标,直到找到searchElement(返回true)
- * 或到达列表末尾而没有找到searchElement(返回false)。
- * 将游标留在搜索期间访问的最后一个元素处。
- */
- @Override
- public boolean find(T searchElement) {
- while (curr <= count) {
- if(storage[curr] == searchElement){
- return true;
- }
- curr++;
- }
- return false;
- }
首先,检查列表是否为空。如果列表为空,直接返回 false。这个操作的时间复杂度为 O(1)。
如果列表不为空,进入循环。循环的次数取决于n,即列表中的元素个数。在每次循环中,进行元素的比较操作 storage[curr] == searchElement,这是一个常数时间操作。如果找到了匹配的元素,返回 true。如果循环完毕仍未找到匹配的元素,返回 false。
综上所述,整个 find 方法的时间复杂度为 O(n)。
- SingleLinkedListSentinel
- /**
- * 前提条件:List不满且newElement不为空。
- * 后置条件:
- * 将newElement插入到游标后面的列表中。如果列表为空,则插入newElement作为列表中的第一个(也是唯一一个)元素。
- * 在两种情况下(空或不空),将光标移动到newElement。
- * 如果列表中没有足够的空间,抛出一个ListException异常。
- */
- @Override
- public void insert(T newElement) throws ListException {
- Node<T> added = new Node<>(newElement, p.next);
- p.next = added;
- p = added;
- }
- /**
- * 先决条件:
- * 列表不是空的。
- * 后置条件:
- * 从列表中移除游标标记的元素。如果结果列表不为空,
- * 然后将光标移动到被删除元素后面的元素。如果删除的元素
- * 位于列表末尾,然后将光标移动到列表开头的元素。
- */
- @Override
- public void remove() {
- if (!isEmpty()) {
- gotoPrev();
- p.next = p.next.next;
- gotoNext();
- if (p.next == tail) {
- gotoBeginning();
- }
- }
- }
- /**
- * 先决条件:
- * List不为空,newElement也不为空。
- * 后置条件:
- * 用newElement替换游标标记的元素。游标保持在newElement处。
- */
- @Override
- public void replace(T newElement) {
- p.value = newElement;
- }
- /**
- * 先决条件:
- * 没有
- * 后置条件:
- * 删除列表中的所有元素。
- */
- @Override
- public void clear() {
- head.next = tail;
- p = head;
- }
insert(T newElement): 插入一个新元素到游标后面的列表中。时间复杂度为 O(1),因为只需要修改几个指针的指向。
remove(): 从列表中移除游标标记的元素。时间复杂度为 O(1),因为只需要修改几个指针的指向。
replace(T newElement): 用新元素替换游标标记的元素。时间复杂度为 O(1),因为只需要修改游标所在节点的值。
clear(): 删除列表中的所有元素。时间复杂度为 O(1),因为只需要修改几个指针的指向。
- @Override
- public boolean isEmpty() {
- return head.next == tail;
- }
- @Override
- public boolean isFull() {
- return false;
- }
- @Override
- public boolean gotoBeginning() {
- if (!isEmpty()) {
- p = head.next;
- return true;
- } else {
- return false;
- }
- }
- @Override
- public boolean gotoEnd() {
- if (!isEmpty()) {
- Node<T> pi = head.next;
- while (pi.next != tail) {
- pi = pi.next;
- }
- p = pi;
- return true;
- } else {
- return false;
- }
- }
- @Override
- public boolean gotoNext() {
- if (p.next != tail) {
- p = p.next;
- return true;
- } else {
- return false;
- }
- }
- @Override
- public boolean gotoPrev() {
- if (p == head || p == null) {
- return false; // 已经是第一个节点或者p为空
- } else {
- Node<T> pi = head;
- while (pi.next != p) {
- pi = pi.next;
- if (pi == null) { // 如果已经到达链表尾部,说明p不是链表中的节点
- return false;
- }
- }
- p = pi; // 现在pi就是p的前一个节点
- return true;
- }
- }
isEmpty(): 检查列表是否为空。时间复杂度为 O(1),因为只需要比较头结点的下一个节点是否为尾节点。
isFull(): 检查列表是否已满。由于该实现是基于链表的,没有固定大小,因此永远不会满。时间复杂度为 O(1)。
gotoBeginning(): 将游标移动到列表开头的元素。时间复杂度为 O(1),因为只需要将游标指向头结点的下一个节点。
gotoEnd(): 将游标移动到列表末尾的元素。时间复杂度为 O(n),其中 n 是列表的长度,因为需要遍历整个链表。
gotoNext(): 将游标移动到下一个元素。时间复杂度为 O(1),因为只需要将游标指向其下一个节点。
gotoPrev(): 将游标移动到前一个元素。时间复杂度为 O(n),其中 n 是列表的长度,因为需要从头开始遍历链表,直到找到游标所在节点的前一个节点。因为这是单向链表才会如此复杂。
- /**
- * 先决条件:
- * 没有
- * 后置条件:
- * 输出列表中的元素和光标的值。如果列表为空,则输出“empty list”。
- * 注意,此操作仅用于测试/调试目的。
- */
- @Override
- public void showStructure(PrintWriter pw) {
- if (isEmpty()) {
- pw.print("Empty list ");
- pw.println("{capacity = 0, length = 0, cursor = -1}");
- } else {
- Node<T> pi = p;
- int current = getN(p);
- gotoEnd();
- int capacity = getN(p) + 1;
- gotoBeginning();
- while (p != tail) {
- pw.print(p.value + " ");
- p = p.next;
- }
- pw.print("{capacity = ");
- pw.print(capacity);
- pw.print(", length = ");
- pw.print(capacity);
- pw.print(", cursor = ");
- pw.print(current);
- pw.println("}");
- p = pi;
- }
- }
首先,检查列表是否为空。这个操作的时间复杂度为 O(1)。
如果列表为空,则输出固定的字符串和列表的一些属性值,这是一个常数时间操作。
如果列表不为空,则调用getN(p)得到p在链表的位置,这个操作的时间复杂度为 O(n);然后将游标移动到列表末尾的元素,时间复杂度为 O(n),再调用getN(p)得到p在链表的位置,加1后就是链表的长度;则将光标 curr 移动到列表的开头,这个操作的时间复杂度为 O(1)。接下来,使用一个循环来遍历列表中的元素,并输出它们。由于遍历的次数取决于列表中的元素个数 n,因此这个操作的时间复杂度为 O(n)。最后,将光标 curr 恢复到原来的位置,输出列表的其他属性值,这都是常数时间操作。
综上所述,当array不为空时,整个 showStructure 方法的时间复杂度为 O(n),当array为空时,方法时间复杂度是O(1)。
- /**
- * 先决条件:
- * 列表至少包含n + 1个元素。
- * 后置条件:
- * 从列表中删除被游标标记的元素,并将其作为列表中的第n个元素重新插入,其中的元素从0开始从头到尾编号。将光标移动到被移动的元素。
- */
- @Override
- public void moveToNth(int n) {
- Node<T> pi = p;
- remove();
- gotoBeginning();
- for (int i = 0; i < n; i++) {
- p = p.next;
- }
- pi.next = p.next;
- p.next = pi;
- p = p.next;
- }
首先将游标所在节点保存到临时变量 pi 中。时间复杂度为 O(1)。调用remove()和gotoBeginning(),时间复杂度为 O(1)。接着将游标移动到第 n 个元素,间复杂度为 O(n)。接下来移动指针,时间复杂度为 O(1)。
综上所述,moveToNth(int n) 方法的时间复杂度为 O(n)。
- /**
- * 先决条件:
- * 列表不是空的。
- * 后置条件:
- * 在列表中搜索searchElement。从游标标记的元素开始搜索。在列表中移动光标,直到找到searchElement(返回true)或到达列表末尾而没有找到searchElement(返回false)。将游标留在搜索期间访问的最后一个元素处。
- */
- @Override
- public boolean find(T searchElement) {
- if (isEmpty()) {
- return false;
- } else {
- while (p != tail) { // 当未到达链表末尾时,继续搜索
- if (p.value.equals(searchElement)) { // 如果找到元素,返回true
- return true;
- }
- p = p.next; // 移动到下一个节点
- }
- return false; // 如果到达链表末尾仍未找到元素,返回false
- }
- }
检查列表是否为空,时间复杂度为 O(1)。当游标未到达链表末尾时,继续搜索,时间复杂度取决于列表中需要遍历的元素数量。 检查当前节点的值是否等于搜索元素,时间复杂度为 O(1)。将游标移动到下一个节点,时间复杂度为 O(1)。
综上所述,find方法的时间复杂度取决于列表中需要遍历的元素数量。如果搜索元素在列表中,则最坏情况下需要遍历整个链表,因此时间复杂度为 O(n),其中 n 是列表的长度。如果搜索元素不在列表中,时间复杂度为 O(n),其中 n 是列表的长度,因为需要遍历整个链表。
- DoubleLinkedListSentinel
- /**
- * 前提条件:List不满且newElement不为空。
- * 后置条件:
- * 将newElement插入到游标后面的列表中。如果列表为空,则插入newElement作为列表中的第一个(也是唯一一个)元素。
- * 在两种情况下(空或不空),将光标移动到newElement。
- * 如果列表中没有足够的空间,抛出一个ListException异常。
- */
- @Override
- public void insert(T newElement) throws ListException {
- Node<T> next = p.next;
- Node<T> added = new Node<>(newElement, p.next, p);
- next.prev = added;
- p.next = added;
- p = added;
- }
- /**
- * 先决条件:
- * 列表不是空的。
- * 后置条件:
- * 从列表中移除游标标记的元素。如果结果列表不为空,
- * 然后将光标移动到被删除元素后面的元素。如果删除的元素
- * 位于列表末尾,然后将光标移动到列表开头的元素。
- */
- @Override
- public void remove() {
- if (!isEmpty()) {
- Node<T> prev = p.prev;
- Node<T> next = p.next;
- prev.next = next;
- next.prev = prev;
- p = next;
- if(p == tail && !isEmpty()){
- gotoBeginning();
- } else if (p == tail && isEmpty()) {
- p = head;
- }
- }
- }
- /**
- * 先决条件:
- * List不为空,newElement也不为空。
- * 后置条件:
- * 用newElement替换游标标记的元素。游标保持在newElement处。
- */
- @Override
- public void replace(T newElement) {
- if (!isEmpty() && newElement != null) {
- p.value = newElement;
- }
- }
- /**
- * 先决条件:
- * 没有
- * 后置条件:
- * 删除列表中的所有元素。
- */
- @Override
- public void clear() {
- head.next = tail;
- tail.prev = head;
- p = head;
- }
- @Override
- public boolean isEmpty() {
- return head.next == tail;
- }
- @Override
- public boolean isFull() {
- return false;
- }
- @Override
- public boolean gotoBeginning() {
- if (!isEmpty()) {
- p = head.next;
- return true;
- } else {
- return false;
- }
- }
- @Override
- public boolean gotoEnd() {
- if (!isEmpty()) {
- p = tail.prev;
- return true;
- } else {
- return false;
- }
- }
- @Override
- public boolean gotoNext() {
- if(isEmpty()){
- return false;
- }
- if (p.next != tail) {
- p = p.next;
- return true;
- } else {
- return false;
- }
- }
- @Override
- public boolean gotoPrev() {
- if(isEmpty()){
- return false;
- }
- if (p.prev != head) {
- p = p.prev;
- return true;
- } else {
- return false;
- }
- }
- /**
- * 先决条件:
- * 列表不是空的。
- * 后置条件:
- * 返回由游标标记的元素的副本。
- */
- @Override
- public T getCursor() {
- if (!isEmpty()) {
- return p.value;
- }
- return null;
- }
- /**
- * 先决条件:
- * 没有
- * 后置条件:
- * 输出列表中的元素和光标的值。如果列表为空,则输出“empty list”。
- * 注意,此操作仅用于测试/调试目的。
- */
- @Override
- public void showStructure(PrintWriter pw) {
- if (isEmpty()) {
- pw.print("Empty list ");
- pw.println("{capacity = 0, length = 0, cursor = -1}");
- } else {
- Node<T> pi = p;
- int current = getN(p);
- gotoEnd();
- int capacity = getN(p) + 1;
- gotoBeginning();
- while (p != tail) {
- pw.print(p.value + " ");
- p = p.next;
- }
- pw.print("{capacity = ");
- pw.print(capacity);
- pw.print(", length = ");
- pw.print(capacity);
- pw.print(", cursor = ");
- pw.print(current);
- pw.println("}");
- p = pi;
- }
- }
- private int getN(Node<T> p) {
- if (head == null) { // 链表为空
- return -1;
- }
- Node<T> pi = head;
- int i = 0;
- while (pi != null && p != pi.next) { // 检查pi是否为null
- pi = pi.next;
- i++;
- }
- // 如果pi为null,说明p不在链表中或者链表已经遍历完
- if (pi == null) {
- return -1;
- }
- return i;
- }
- /**
- * 先决条件:
- * 列表至少包含n + 1个元素。
- * 后置条件:
- * 从列表中删除被游标标记的元素,并将其作为列表中的第n个元素重新插入,其中的元素从0开始从头到尾编号。将光标移动到被移动的元素。
- */
- @Override
- public void moveToNth(int n) {
- Node<T> pi = p;
- remove();
- gotoBeginning();
- for (int i = 0; i < n; i++) {
- p = p.next;
- }
- Node<T> pj = p.next;
- pi.next = p.next;
- p.next = pi;
- pj.prev = pi;
- pi.prev = p;
- p = p.next;
- }
- /**
- * 先决条件:
- * 列表不是空的。
- * 后置条件:
- * 在列表中搜索searchElement。从游标标记的元素开始搜索。在列表中移动光标,直到找到searchElement(返回true)或到达列表末尾而没有找到searchElement(返回false)。将游标留在搜索期间访问的最后一个元素处。
- */
- @Override
- public boolean find(T searchElement) {
- if (isEmpty()) {
- return false;
- } else {
- while (p != tail) { // 当未到达链表末尾时,继续搜索
- if (p.value.equals(searchElement)) { // 如果找到元素,返回true
- return true;
- }
- p = p.next; // 移动到下一个节点
- }
- return false; // 如果到达链表末尾仍未找到元素,返回false
- }
- }
双向链表和单向链表大部分都方法实现都很类似,由于头尾节点和双向指针的存在,gotoEnd和gotoPrev方法可以在时间复杂度为O(1)的情况下实现。showStructure、moveToNth、find方法虽然仍然是O(n),但是相对来说比单向链表快一些。
首先测试Array:
我们可以直接把输出打印在控制台,结果如下:
用人眼无法看出来我们打印在控制台的和答案是否相同,所以我们将PrintWriter打印到一个文件output.txt中,再比较两个文件是否相同。为此写了一个类TextFileComparator。
- package Homework0102;
- import java.io.BufferedReader;
- import java.io.FileReader;
- import java.io.IOException;
- public class TextFileComparator {
- public static boolean compareFiles(String filePath1, String filePath2) {
- try (BufferedReader reader1 = new BufferedReader(new FileReader(filePath1));
- BufferedReader reader2 = new BufferedReader(new FileReader(filePath2))) {
- String line1, line2;
- while ((line1 = reader1.readLine()) != null) {
- line2 = reader2.readLine();
- if (!line1.equals(line2)) {
- return false;
- }
- }
- // Check if file2 has additional lines
- return reader2.readLine() == null;
- } catch (IOException e) {
- e.printStackTrace();
- return false;
- }
- }
- public static void main(String[] args) {
- String file1 = "D:\\develop\\projects\\dataStructure\\homework02\\src\\Homework0102\\list_result.txt";
- String file2 = "D:\\develop\\projects\\dataStructure\\homework02\\src\\Homework0102\\output.txt";
- boolean areEqual = compareFiles(file1, file2);
- if (areEqual) {
- System.out.println("两个文件的内容相同");
- } else {
- System.out.println("两个文件的内容不同");
- }
- }
- }
结果如下:
测试SingleLinkedListSentinel和DoubleLinkedListSentinel:
链表的capacity总会与count相同,但是我们缺少相对应的文件,所以不能再用上述的方式看是否正确。我们只能用肉眼看。结果分别如下:
SingleLinkedListSentinel的测试用例额对应的测试结果(部分)
DoubleLinkedListSentinel的测试用例额对应的测试结果(部分)
Array |
SingleLinkedListSentinel |
DoubleLinkedListSentinel | |
insert |
O(n) |
O(1) |
O(1) |
remove |
O(n) |
O(1) |
O(1) |
replace |
O(1) |
O(1) |
O(1) |
clear |
O(n) |
O(1) |
O(1) |
isEmpty |
O(1) |
O(1) |
O(1) |
isFull |
O(1) |
O(1) |
O(1) |
gotoBeginning |
O(1) |
O(1) |
O(1) |
gotoEnd |
O(1) |
O(n) |
O(1) |
gotoNext |
O(1) |
O(1) |
O(1) |
gotoPrev |
O(1) |
O(n) |
O(1) |
getCursor |
O(1) |
O(1) |
O(1) |
showStructure |
O(n) |
O(n) |
O(n) |
moveToNth |
O(n) |
O(n) |
O(n) |
find |
O(n) |
O(n) |
O(n) |
通过这个实验,首先练习三种数据结构,并学会了泛型,复习了内部类,学习了StreamTokenizer类的使用,学会了文件相关的方法和流。收获很大。
任务2
- 题目
观察任务 1 中基于数组实现的线性表的测试用例的运行结果,发现大部分时候空间的使用率是不高的(length 和 capacity 的比值反映了这一事实),而且还存在有空间不够用的例外发生。当然,基于链式存储实现的线性表则不存在此类问题。为了解决空间利用率以及空间不够用的问题,任务 2 将使用动态调整的方式改善数组空间的大小,方案可以有很多种,但在本次实验中将采用如下的调整方案,具体步骤如下:
① 使用 capacity 表示当前线性表的最大容量(即最多能够存储的线性表元素个数);
② 初始情况下,capacity=1;
③ 当插入元素时线性表满,那么就重新生成一个容量为 2*capacity 的数组,将原数组中的capacity 个元素拷贝到新数组中,让新数组成为当前线性表的存储表示;
④ 当删除元素之后,如果当前线性表中的元素个数 length 是 capacity 的四分之一时,则重新生成一个容量为 capacity/2 的数组,将原数组中的 length 个元素拷贝到新数组中,让新数组成为当前线性表的存储表示。
如果基于数组存储表示的线性表按照如上的方式完成空间的动态调整,那么构造方法中就不需要再指定初始空间的大小了。
该任务中需要完成的工作如下:
① 按照任务 1 中的 List 接口定义,实现一个 ResizingAList 线性表,数组空间的调整方案如该任务中所描述的;
② 继续使用“list_testcase.txt”进行测试,并将结果中的每行运行结果中当前线性表的空间使用率和任务 1 中的空间使用率用图的方式展示其变化过程。
- 数据设计
ResizingAList和Array本质没有区别,只需要实现一个方法resizeStorage,根据传入的参数newCapacity重新生成一个数组,将原数组中的capacity 个元素拷贝到新数组中,让新数组成为当前线性表的存储表示即可。最开始数组大小为1,然后在insert和remove方法判断条件并调用即可。
resizeStorage方法:
- /**
- * 重新生成一个指定容量的数组,并将原数组中的元素拷贝到新数组中。
- *
- * @param newCapacity 新的数组容量。
- */
- private void resizeStorage(int newCapacity) {
- if (newCapacity < count) {
- throw new IllegalArgumentException("New capacity is too small to hold existing elements.");
- }
- T[] newStorage = (T[]) new Object[newCapacity];
- for (int i = 0; i < count; i++) {
- newStorage[i] = storage[i];
- }
- storage = newStorage;
- capacity = newCapacity;
- }
构造方法:
- //不带初始容量的构造方法。默认容量为1
- public DynamicArray() {
- this.capacity = 1;
- this.curr = -1;
- this.storage = (T[]) new Object[1];
- this.count = 0;
- }
Insert方法:
- /**
- * 在当前位置插入一个元素
- * 从curr开始的元素全部向后移动一位
- * curr指向被插入的元素
- */
- @Override
- public void insert(T newElement) throws ListException {
- //列表满了
- if (isFull()) {
- resizeStorage(capacity * 2);
- }
- if (curr < -1 || curr > capacity) {
- throw new ListException("插入位置不合适");
- }
- if(isEmpty()){
- curr = -1;
- }
- for (int i = count - 1; i > curr; i--) {
- storage[i + 1] = storage[i];
- }
- curr++;
- storage[curr] = newElement;
- count++;
- }
Remove方法:
- /**
- * 从列表中移除游标标记的元素
- * 如果结果列表不为空,则将光标移动到被删除元素后面的元素
- * 如果被删除的元素位于列表末尾,则将光标移动到列表开头的元素
- */
- @Override
- public void remove() {
- //如果列表不为空就把后面的元素前移就行,如果已经空了就不用管了
- if (!isEmpty()) {
- for (int i = curr + 1; i < count; i++) {
- storage[i - 1] = storage[i];
- }
- count--;
- if (count == curr) {
- curr = 0;
- }
- // 当删除元素后,检查是否需要缩小数组的容量
- if (count > 0 && count == capacity / 4) {
- resizeStorage(capacity / 2);
- }
- }
- }
- 算法设计
作出空间使用率的图的思路如下:
提供的文件有很多行命令行,每一行都会打印一个capacity和length,也就可以得到一个比值代表空间使用率,那么我们就将行数代表横轴,纵轴是空间使用率。
我们在Array和DynamicArray中各自写一个方法SpaceUsageOfArray,输出length/capacity表示空间使用率。
- public double SpaceUsageOfArray(){
- return (double) count / capacity;
- }
对于Array和DynamicArray,在每行读入数据处理后分别调用SpaceUsageOfArray得到空间使用率并储存进DynamicArray类型的UsageOfArray和UsageOfDynamicArray中,不直接导入数组的原因是我们无法知道行数是多少。
- String line;
- while ((line = br.readLine()) != null) {
- executeCommand(array, line);
- executeCommand(dynamicArray, line, writer);
- UsageOfArray.insert(array.SpaceUsageOfArray());
- UsageOfDynamicArray.insert(dynamicArray.SpaceUsageOfArray());
- writer.flush(); // 刷新PrintWriter的缓冲区
- }
因为空间使用率在DynamicArray类型的数组中,我们就得到了行数numberOfLine,并创建两个数组储存对应的数据,此时我们已经准备好了两个纵坐标的数据。我们再创建一个数组表示横坐标,储存从1到行数到数即可。
- int numberOfLine = UsageOfDynamicArray.getCount();
- re = new double[numberOfLine];//一共有125行横坐标有125个
- arrayUsageOfArray = new double[numberOfLine];
- arrayUsageOfDynamicArray = new double[numberOfLine];
- for (int i = 0; i < re.length; i++) {
- re[i] = i + 1;
- }
- UsageOfArray.gotoBeginning();
- for (int i = 0; i < arrayUsageOfArray.length; i++) {
- arrayUsageOfArray[i] = UsageOfArray.getCursor();
- UsageOfArray.gotoNext();
- }
- UsageOfDynamicArray.gotoBeginning();
- for (int i = 0; i < arrayUsageOfDynamicArray.length; i++) {
- arrayUsageOfDynamicArray[i] = UsageOfDynamicArray.getCursor();
- UsageOfDynamicArray.gotoNext();
- }
我们调用上次作业的代码作图即可。
- Draw demo = new Draw("UsageOfArray");
- demo.pack();
- demo.setVisible(true);
- 运行结果展示
DynamicArray的测试用例额对应的测试结果(部分)
每行运行结果中当前线性表的空间使用率和任务 1 中的空间使用率用图的方式展示其变化过程:
DynamicArray和Array的空间利用率对比图
- 总结和收获
可以看出来,动态数组的空间利用率远远大于静态数组。
复习了如何利用java作图,学习了动态数组和静态数组。
附录:
- 任务1、2
- 老师提供的List不再展示:
- ListException:
- package Homework0102;
- public class ListException extends Throwable {
- public ListException(String message) {
- super(message);
- }
- }
- Array:
- package Homework0102;
- import java.io.PrintWriter;
- import java.util.Arrays;
- import java.util.Iterator;
- public class Array<T> implements List<T>, Iterable<T> {
- private T[] storage;//储存的数组
- private int capacity;//容量
- private int count;//元素数量
- private int curr;//当前元素的位置
- //不带初始容量的构造方法。默认容量为512
- public Array() {
- this.capacity = 512;
- this.curr = -1;
- this.storage = (T[]) new Object[512];
- this.count = 0;
- }
- /**
- * 在当前位置插入一个元素
- * 从curr开始的元素全部向后移动一位
- * curr指向被插入的元素
- */
- @Override
- public void insert(T newElement) throws ListException {
- //列表满了
- if (isFull()) {
- return;
- }
- if (curr < -1 || curr > capacity) {
- throw new ListException("插入位置不合适");
- }
- if(isEmpty()){
- curr = -1;
- }
- for (int i = count - 1; i > curr; i--) {
- storage[i + 1] = storage[i];
- }
- curr++;
- storage[curr] = newElement;
- count++;
- }
- /**
- * 从列表中移除游标标记的元素
- * 如果结果列表不为空,则将光标移动到被删除元素后面的元素
- * 如果被删除的元素位于列表末尾,则将光标移动到列表开头的元素
- */
- @Override
- public void remove() {
- //如果列表不为空就把后面的元素前移就行,如果已经空了就不用管了
- if (!isEmpty()) {
- for (int i = curr + 1; i < count; i++) {
- storage[i - 1] = storage[i];
- }
- count--;
- if (count == curr) {
- curr = 0;
- }
- }
- }
- /**
- * 先决条件:List不为空,newElement也不为空。
- * 后置条件:用newElement替换游标标记的元素。游标保持在newElement处
- */
- @Override
- public void replace(T newElement) {
- if(isEmpty()){
- return;
- }
- storage[curr] = newElement;
- }
- //删除列表中所有元素
- @Override
- public void clear() {
- Arrays.fill(storage, 0, count, null); // 将数组中的元素设为null
- count = curr = 0;//元素清空
- }
- @Override
- public boolean isEmpty() {
- return count == 0;
- }
- @Override
- public boolean isFull() {
- return count == capacity;
- }
- //如果列表不为空,则将光标移动到列表的开头并返回true。否则,返回false。
- @Override
- public boolean gotoBeginning() {
- if (!isEmpty()) {
- curr = 0;
- return true;
- } else {
- return false;
- }
- }
- @Override
- public boolean gotoEnd() {
- if (!isEmpty()) {
- curr = count - 1;
- return true;
- } else {
- return false;
- }
- }
- @Override
- public boolean gotoNext() {
- if (isEmpty() || curr == count - 1) {
- return false;
- }
- curr++;
- return true;
- }
- @Override
- public boolean gotoPrev() {
- if (isEmpty() || curr == 0) {
- return false;
- }
- curr--;
- return true;
- }
- /**
- * 前提条件: List不为空。
- * 返回游标所标记元素的副本。
- */
- @Override
- public T getCursor() {
- return storage[curr];
- }
- /**
- * 输出列表中的元素和游标的值。
- * 如果列表为空,则输出“empty list”。
- * 注意,此操作仅用于测试/调试目的。
- */
- @Override
- public void showStructure(PrintWriter pw) {
- int current = curr;
- if (isEmpty()) {
- pw.print("Empty list ");
- pw.print("{capacity = ");
- pw.print(capacity);
- pw.print(", length = ");
- pw.print(count);
- pw.print(", cursor = ");
- pw.print(-1);
- pw.println("}");
- } else {
- gotoBeginning();
- for (int i = 0; i < count; i++) {
- pw.print(storage[i] + " ");
- }
- curr = current;
- pw.print("{capacity = ");
- pw.print(capacity);
- pw.print(", length = ");
- pw.print(count);
- pw.print(", cursor = ");
- pw.print(curr);
- pw.println("}");
- }
- }
- public double SpaceUsageOfArray(){
- return (double) count / capacity;
- }
- /**
- * 前提条件:列表至少包含n + 1个元素。
- * 从列表中删除游标标记的元素
- * 并将其作为列表中的第n个元素重新插入
- * 其中的元素从0开始从头到尾编号
- * 将光标移动到被移动的元素。
- */
- @Override
- public void moveToNth(int n) throws ListException {
- if (count < n + 1) {
- System.out.println("输入的n过大");
- return;
- }
- T removed = getCursor();
- remove();
- gotoN(n);
- insert(removed);
- }
- /**
- * 前提条件: List不为空。
- * 在列表中搜索searchElement。
- * 从游标标记的元素开始搜索。在列表中移动光标,直到找到searchElement(返回true)
- * 或到达列表末尾而没有找到searchElement(返回false)。
- * 将游标留在搜索期间访问的最后一个元素处。
- */
- @Override
- public boolean find(T searchElement) {
- while (curr <= count) {
- if(storage[curr] == searchElement){
- return true;
- }
- curr++;
- }
- return false;
- }
- @Override
- public Iterator<T> iterator() {
- curr = 0;
- return new Iterator<T>() {
- @Override
- public boolean hasNext() {
- return curr < count;
- }
- @Override
- public T next() {
- T value = storage[curr];
- curr++;
- return value;
- }
- };
- }
- public void gotoN(int n) {
- gotoBeginning();
- for (int i = 0; i < n; i++) {
- gotoNext();
- }
- }
- }
- SingleLinkedListSentinel:
- package Homework0102;
- import java.io.PrintWriter;
- import java.util.Iterator;
- //单向链表带头尾哨兵
- public class SingleLinkedListSentinel<T> implements List<T>, Iterable<T> {
- private static class Node<T> {
- T value;
- Node<T> next;
- public Node(T value, Node<T> next) {
- this.value = value;
- this.next = next;
- }
- }
- //两个哨兵节点
- private Node<T> head;
- private Node<T> tail;
- private Node<T> p;//指针
- public SingleLinkedListSentinel() {
- head = new Node<T>(null, null);
- tail = new Node<T>(null, null);
- head.next = tail;
- p = head;
- }
- /**
- * 前提条件:List不满且newElement不为空。
- * 后置条件:
- * 将newElement插入到游标后面的列表中。如果列表为空,则插入newElement作为列表中的第一个(也是唯一一个)元素。
- * 在两种情况下(空或不空),将光标移动到newElement。
- * 如果列表中没有足够的空间,抛出一个ListException异常。
- */
- @Override
- public void insert(T newElement) throws ListException {
- Node<T> added = new Node<>(newElement, p.next);
- p.next = added;
- p = added;
- }
- /**
- * 先决条件:
- * 列表不是空的。
- * 后置条件:
- * 从列表中移除游标标记的元素。如果结果列表不为空,
- * 然后将光标移动到被删除元素后面的元素。如果删除的元素
- * 位于列表末尾,然后将光标移动到列表开头的元素。
- */
- @Override
- public void remove() {
- if (!isEmpty()) {
- gotoPrev();
- p.next = p.next.next;
- gotoNext();
- if (p.next == tail) {
- gotoBeginning();
- }
- }
- }
- /**
- * 先决条件:
- * List不为空,newElement也不为空。
- * 后置条件:
- * 用newElement替换游标标记的元素。游标保持在newElement处。
- */
- @Override
- public void replace(T newElement) {
- p.value = newElement;
- }
- /**
- * 先决条件:
- * 没有
- * 后置条件:
- * 删除列表中的所有元素。
- */
- @Override
- public void clear() {
- head.next = tail;
- p = head;
- }
- @Override
- public boolean isEmpty() {
- return head.next == tail;
- }
- @Override
- public boolean isFull() {
- return false;
- }
- @Override
- public boolean gotoBeginning() {
- if (!isEmpty()) {
- p = head.next;
- return true;
- } else {
- return false;
- }
- }
- @Override
- public boolean gotoEnd() {
- if (!isEmpty()) {
- Node<T> pi = head.next;
- while (pi.next != tail) {
- pi = pi.next;
- }
- p = pi;
- return true;
- } else {
- return false;
- }
- }
- @Override
- public boolean gotoNext() {
- if (p.next != tail) {
- p = p.next;
- return true;
- } else {
- return false;
- }
- }
- @Override
- public boolean gotoPrev() {
- if (p == head || p == null) {
- return false; // 已经是第一个节点或者p为空
- } else {
- Node<T> pi = head;
- while (pi.next != p) {
- pi = pi.next;
- if (pi == null) { // 如果已经到达链表尾部,说明p不是链表中的节点
- return false;
- }
- }
- p = pi; // 现在pi就是p的前一个节点
- return true;
- }
- }
- /**
- * 先决条件:
- * 列表不是空的。
- * 后置条件:
- * 返回由游标标记的元素的副本。
- */
- @Override
- public T getCursor() {
- return p.value;
- }
- /**
- * 先决条件:
- * 没有
- * 后置条件:
- * 输出列表中的元素和光标的值。如果列表为空,则输出“empty list”。
- * 注意,此操作仅用于测试/调试目的。
- */
- @Override
- public void showStructure(PrintWriter pw) {
- if (isEmpty()) {
- pw.print("Empty list ");
- pw.println("{capacity = 0, length = 0, cursor = -1}");
- } else {
- Node<T> pi = p;
- int current = getN(p);
- gotoEnd();
- int capacity = getN(p) + 1;
- gotoBeginning();
- while (p != tail) {
- pw.print(p.value + " ");
- p = p.next;
- }
- pw.print("{capacity = ");
- pw.print(capacity);
- pw.print(", length = ");
- pw.print(capacity);
- pw.print(", cursor = ");
- pw.print(current);
- pw.println("}");
- p = pi;
- }
- }
- /**
- * 先决条件:
- * 列表至少包含n + 1个元素。
- * 后置条件:
- * 从列表中删除被游标标记的元素,并将其作为列表中的第n个元素重新插入,其中的元素从0开始从头到尾编号。将光标移动到被移动的元素。
- */
- @Override
- public void moveToNth(int n) {
- Node<T> pi = p;
- remove();
- gotoBeginning();
- for (int i = 0; i < n; i++) {
- p = p.next;
- }
- pi.next = p.next;
- p.next = pi;
- p = p.next;
- }
- /**
- * 先决条件:
- * 列表不是空的。
- * 后置条件:
- * 在列表中搜索searchElement。从游标标记的元素开始搜索。在列表中移动光标,直到找到searchElement(返回true)或到达列表末尾而没有找到searchElement(返回false)。将游标留在搜索期间访问的最后一个元素处。
- */
- @Override
- public boolean find(T searchElement) {
- if (isEmpty()) {
- return false;
- } else {
- while (p != tail) { // 当未到达链表末尾时,继续搜索
- if (p.value.equals(searchElement)) { // 如果找到元素,返回true
- return true;
- }
- p = p.next; // 移动到下一个节点
- }
- return false; // 如果到达链表末尾仍未找到元素,返回false
- }
- }
- private int getN(Node<T> p) {
- if (head == null) { // 链表为空
- return -1;
- }
- Node<T> pi = head;
- int i = 0;
- while (pi != null && p != pi.next) { // 检查pi是否为null
- pi = pi.next;
- i++;
- }
- // 如果pi为null,说明p不在链表中或者链表已经遍历完
- if (pi == null) {
- return -1;
- }
- return i;
- }
- @Override
- public Iterator<T> iterator() {
- return new Iterator<T>() {
- Node<T> p = head.next;
- @Override
- public boolean hasNext() {
- return p != tail;
- }
- @Override
- public T next() {
- T value = p.value;
- p = p.next;
- return value;
- }
- };
- }
- }
- DoubleLinkedListSentinel:
- package Homework0102;
- import java.io.PrintWriter;
- import java.util.Iterator;
- //双向链表带头尾哨兵
- public class DoubleLinkedListSentinel<T> implements List<T>, Iterable<T> {
- private static class Node<T> {
- T value;
- Node<T> next;
- Node<T> prev;
- public Node(T value, Node<T> next, Node<T> prev) {
- this.value = value;
- this.next = next;
- this.prev = prev;
- }
- }
- //两个哨兵节点
- private Node<T> head;
- private Node<T> tail;
- private Node<T> p;//指针
- public DoubleLinkedListSentinel() {
- head = new Node<T>(null, null, null);
- tail = new Node<T>(null, null, null);
- head.next = tail;
- tail.prev = head;
- p = head;
- }
- /**
- * 前提条件:List不满且newElement不为空。
- * 后置条件:
- * 将newElement插入到游标后面的列表中。如果列表为空,则插入newElement作为列表中的第一个(也是唯一一个)元素。
- * 在两种情况下(空或不空),将光标移动到newElement。
- * 如果列表中没有足够的空间,抛出一个ListException异常。
- */
- @Override
- public void insert(T newElement) throws ListException {
- Node<T> next = p.next;
- Node<T> added = new Node<>(newElement, p.next, p);
- next.prev = added;
- p.next = added;
- p = added;
- }
- /**
- * 先决条件:
- * 列表不是空的。
- * 后置条件:
- * 从列表中移除游标标记的元素。如果结果列表不为空,
- * 然后将光标移动到被删除元素后面的元素。如果删除的元素
- * 位于列表末尾,然后将光标移动到列表开头的元素。
- */
- @Override
- public void remove() {
- if (!isEmpty()) {
- Node<T> prev = p.prev;
- Node<T> next = p.next;
- prev.next = next;
- next.prev = prev;
- p = next;
- if(p == tail && !isEmpty()){
- gotoBeginning();
- } else if (p == tail && isEmpty()) {
- p = head;
- }
- }
- }
- /**
- * 先决条件:
- * List不为空,newElement也不为空。
- * 后置条件:
- * 用newElement替换游标标记的元素。游标保持在newElement处。
- */
- @Override
- public void replace(T newElement) {
- if (!isEmpty() && newElement != null) {
- p.value = newElement;
- }
- }
- /**
- * 先决条件:
- * 没有
- * 后置条件:
- * 删除列表中的所有元素。
- */
- @Override
- public void clear() {
- head.next = tail;
- tail.prev = head;
- p = head;
- }
- @Override
- public boolean isEmpty() {
- return head.next == tail;
- }
- @Override
- public boolean isFull() {
- return false;
- }
- @Override
- public boolean gotoBeginning() {
- if (!isEmpty()) {
- p = head.next;
- return true;
- } else {
- return false;
- }
- }
- @Override
- public boolean gotoEnd() {
- if (!isEmpty()) {
- p = tail.prev;
- return true;
- } else {
- return false;
- }
- }
- @Override
- public boolean gotoNext() {
- if(isEmpty()){
- return false;
- }
- if (p.next != tail) {
- p = p.next;
- return true;
- } else {
- return false;
- }
- }
- @Override
- public boolean gotoPrev() {
- if(isEmpty()){
- return false;
- }
- if (p.prev != head) {
- p = p.prev;
- return true;
- } else {
- return false;
- }
- }
- /**
- * 先决条件:
- * 列表不是空的。
- * 后置条件:
- * 返回由游标标记的元素的副本。
- */
- @Override
- public T getCursor() {
- if (!isEmpty()) {
- return p.value;
- }
- return null;
- }
- /**
- * 先决条件:
- * 没有
- * 后置条件:
- * 输出列表中的元素和光标的值。如果列表为空,则输出“empty list”。
- * 注意,此操作仅用于测试/调试目的。
- */
- @Override
- public void showStructure(PrintWriter pw) {
- if (isEmpty()) {
- pw.print("Empty list ");
- pw.println("{capacity = 0, length = 0, cursor = -1}");
- } else {
- Node<T> pi = p;
- int current = getN(p);
- gotoEnd();
- int capacity = getN(p) + 1;
- gotoBeginning();
- while (p != tail) {
- pw.print(p.value + " ");
- p = p.next;
- }
- pw.print("{capacity = ");
- pw.print(capacity);
- pw.print(", length = ");
- pw.print(capacity);
- pw.print(", cursor = ");
- pw.print(current);
- pw.println("}");
- p = pi;
- }
- }
- private int getN(Node<T> p) {
- if (head == null) { // 链表为空
- return -1;
- }
- Node<T> pi = head;
- int i = 0;
- while (pi != null && p != pi.next) { // 检查pi是否为null
- pi = pi.next;
- i++;
- }
- // 如果pi为null,说明p不在链表中或者链表已经遍历完
- if (pi == null) {
- return -1;
- }
- return i;
- }
- /**
- * 先决条件:
- * 列表至少包含n + 1个元素。
- * 后置条件:
- * 从列表中删除被游标标记的元素,并将其作为列表中的第n个元素重新插入,其中的元素从0开始从头到尾编号。将光标移动到被移动的元素。
- */
- @Override
- public void moveToNth(int n) {
- Node<T> pi = p;
- remove();
- gotoBeginning();
- for (int i = 0; i < n; i++) {
- p = p.next;
- }
- Node<T> pj = p.next;
- pi.next = p.next;
- p.next = pi;
- pj.prev = pi;
- pi.prev = p;
- p = p.next;
- }
- /**
- * 先决条件:
- * 列表不是空的。
- * 后置条件:
- * 在列表中搜索searchElement。从游标标记的元素开始搜索。在列表中移动光标,直到找到searchElement(返回true)或到达列表末尾而没有找到searchElement(返回false)。将游标留在搜索期间访问的最后一个元素处。
- */
- @Override
- public boolean find(T searchElement) {
- if (isEmpty()) {
- return false;
- } else {
- while (p != tail) { // 当未到达链表末尾时,继续搜索
- if (p.value.equals(searchElement)) { // 如果找到元素,返回true
- return true;
- }
- p = p.next; // 移动到下一个节点
- }
- return false; // 如果到达链表末尾仍未找到元素,返回false
- }
- }
- @Override
- public Iterator<T> iterator() {
- return new Iterator<T>() {
- Node<T> p = head.next;
- @Override
- public boolean hasNext() {
- return p != tail;
- }
- @Override
- public T next() {
- T value = p.value;
- p = p.next;
- return value;
- }
- };
- }
- }
- DynamicArray:
- package Homework0102;
- import java.io.PrintWriter;
- import java.util.Arrays;
- import java.util.Iterator;
- public class DynamicArray<T> implements List<T>, Iterable<T> {
- private T[] storage;//储存的数组
- private int capacity;//容量
- private int count;//元素数量
- private int curr;//当前元素的位置
- //不带初始容量的构造方法。默认容量为1
- public DynamicArray() {
- this.capacity = 1;
- this.curr = -1;
- this.storage = (T[]) new Object[1];
- this.count = 0;
- }
- /**
- * 在当前位置插入一个元素
- * 从curr开始的元素全部向后移动一位
- * curr指向被插入的元素
- */
- @Override
- public void insert(T newElement) throws ListException {
- //列表满了
- if (isFull()) {
- resizeStorage(capacity * 2);
- }
- if (curr < -1 || curr > capacity) {
- throw new ListException("插入位置不合适");
- }
- if(isEmpty()){
- curr = -1;
- }
- for (int i = count - 1; i > curr; i--) {
- storage[i + 1] = storage[i];
- }
- curr++;
- storage[curr] = newElement;
- count++;
- }
- /**
- * 从列表中移除游标标记的元素
- * 如果结果列表不为空,则将光标移动到被删除元素后面的元素
- * 如果被删除的元素位于列表末尾,则将光标移动到列表开头的元素
- */
- @Override
- public void remove() {
- //如果列表不为空就把后面的元素前移就行,如果已经空了就不用管了
- if (!isEmpty()) {
- for (int i = curr + 1; i < count; i++) {
- storage[i - 1] = storage[i];
- }
- count--;
- if (count == curr) {
- curr = 0;
- }
- // 当删除元素后,检查是否需要缩小数组的容量
- if (count > 0 && count == capacity / 4) {
- resizeStorage(capacity / 2);
- }
- }
- }
- /**
- * 重新生成一个指定容量的数组,并将原数组中的元素拷贝到新数组中。
- *
- * @param newCapacity 新的数组容量。
- */
- private void resizeStorage(int newCapacity) {
- if (newCapacity < count) {
- throw new IllegalArgumentException("New capacity is too small to hold existing elements.");
- }
- T[] newStorage = (T[]) new Object[newCapacity];
- for (int i = 0; i < count; i++) {
- newStorage[i] = storage[i];
- }
- storage = newStorage;
- capacity = newCapacity;
- }
- /**
- * 先决条件:List不为空,newElement也不为空。
- * 后置条件:用newElement替换游标标记的元素。游标保持在newElement处
- */
- @Override
- public void replace(T newElement) {
- if(!isEmpty()){
- storage[curr] = newElement;
- }
- }
- //删除列表中所有元素
- @Override
- public void clear() {
- Arrays.fill(storage, 0, count, null); // 将数组中的元素设为null
- count = curr = 0; // 元素数量和当前位置重置为0
- capacity = 1; // 容量重置为初始值
- storage = (T[]) new Object[capacity]; // 重新创建一个新的数组
- }
- @Override
- public boolean isEmpty() {
- return count == 0;
- }
- @Override
- public boolean isFull() {
- return count == capacity;
- }
- //如果列表不为空,则将光标移动到列表的开头并返回true。否则,返回false。
- @Override
- public boolean gotoBeginning() {
- if (!isEmpty()) {
- curr = 0;
- return true;
- } else {
- return false;
- }
- }
- //
- @Override
- public boolean gotoEnd() {
- if (!isEmpty()) {
- curr = count - 1;
- return true;
- } else {
- return false;
- }
- }
- @Override
- public boolean gotoNext() {
- if (isEmpty() || curr == count - 1) {
- return false;
- }
- curr++;
- return true;
- }
- @Override
- public boolean gotoPrev() {
- if (isEmpty() || curr == 0) {
- return false;
- }
- curr--;
- return true;
- }
- /**
- * 前提条件: List不为空。
- * 返回游标所标记元素的副本。
- */
- @Override
- public T getCursor() {
- return storage[curr];
- }
- /**
- * 输出列表中的元素和游标的值。
- * 如果列表为空,则输出“empty list”。
- * 注意,此操作仅用于测试/调试目的。
- */
- @Override
- public void showStructure(PrintWriter pw) {
- int current = curr;
- if (isEmpty()) {
- pw.print("Empty list");
- pw.print("{capacity = ");
- pw.print(capacity);
- pw.print(", length = ");
- pw.print(count);
- pw.print(", cursor = ");
- pw.print(-1);
- pw.println("}");
- } else {
- gotoBeginning();
- for (int i = 0; i < count; i++) {
- pw.print(storage[i] + " ");
- }
- curr = current;
- pw.print("{capacity = ");
- pw.print(capacity);
- pw.print(", length = ");
- pw.print(count);
- pw.print(", cursor = ");
- pw.print(curr);
- pw.println("}");
- }
- }
- public int getCount() {
- return count;
- }
- public double SpaceUsageOfArray(){
- if(isEmpty()){
- return 1.0;
- }
- return (double) count / capacity;
- }
- /**
- * 前提条件:列表至少包含n + 1个元素。
- * 从列表中删除游标标记的元素
- * 并将其作为列表中的第n个元素重新插入
- * 其中的元素从0开始从头到尾编号
- * 将光标移动到被移动的元素。
- */
- @Override
- public void moveToNth(int n) throws ListException {
- if (count < n + 1) {
- System.out.println("输入的n过大");
- return;
- }
- T removed = getCursor();
- remove();
- gotoN(n);
- insert(removed);
- }
- /**
- * 前提条件: List不为空。
- * 在列表中搜索searchElement。
- * 从游标标记的元素开始搜索。在列表中移动光标,直到找到searchElement(返回true)
- * 或到达列表末尾而没有找到searchElement(返回false)。
- * 将游标留在搜索期间访问的最后一个元素处。
- */
- @Override
- public boolean find(T searchElement) {
- while (curr <= count) {
- if (storage[curr] == searchElement) {
- return true;
- }
- curr++;
- }
- return false;
- }
- @Override
- public Iterator<T> iterator() {
- curr = 0;
- return new Iterator<T>() {
- @Override
- public boolean hasNext() {
- return curr < count;
- }
- @Override
- public T next() {
- T value = storage[curr];
- curr++;
- return value;
- }
- };
- }
- public void gotoN(int n) {
- gotoBeginning();
- for (int i = 0; i < n; i++) {
- gotoNext();
- }
- }
- }
- 测试类FileTest:
- package Homework0102;
- import java.io.*;
- public class FileTest {
- public static void main(String[] args) throws IOException, ListException {
- DynamicArray<Object> array = new DynamicArray<>();
- FileReader fis = new FileReader("D:\\develop\\projects\\dataStructure\\homework02\\src\\Homework0102\\list_testcase.txt");
- BufferedReader br = new BufferedReader(fis);
- PrintWriter writer = new PrintWriter(System.out); // 创建PrintWriter对象
- String line;
- while ((line = br.readLine()) != null) {
- executeCommand(array, line);
- array.showStructure(writer); // 调用array.showStructure(writer)显示列表结构
- writer.flush(); // 刷新PrintWriter的缓冲区
- }
- writer.close(); // 在循环结束后关闭PrintWriter对象
- }
- private static void executeCommand(DynamicArray<Object> array, String line) throws ListException, IOException {
- StreamTokenizer st = new StreamTokenizer(new StringReader(line));
- int token = st.nextToken();
- while (token != StreamTokenizer.TT_EOF) {
- char command = (char) st.ttype;
- switch (command) {
- case '+' -> {
- st.nextToken();
- array.insert(st.sval);
- }
- case '-' -> array.remove();
- case '=' -> {
- st.nextToken();
- array.replace(st.sval);
- }
- case '#' -> array.gotoBeginning();
- case '*' -> array.gotoEnd();
- case '>' -> array.gotoNext();
- case '<' -> array.gotoPrev();
- case '~' -> array.clear();
- }
- token = st.nextToken();
- }
- }
- }
- 文件比较类TextFileComparator:
- package Homework0102;
- import java.io.BufferedReader;
- import java.io.FileReader;
- import java.io.IOException;
- public class TextFileComparator {
- public static boolean compareFiles(String filePath1, String filePath2) {
- try (BufferedReader reader1 = new BufferedReader(new FileReader(filePath1));
- BufferedReader reader2 = new BufferedReader(new FileReader(filePath2))) {
- String line1, line2;
- while ((line1 = reader1.readLine()) != null) {
- line2 = reader2.readLine();
- if (!line1.equals(line2)) {
- return false;
- }
- }
- // Check if file2 has additional lines
- return reader2.readLine() == null;
- } catch (IOException e) {
- e.printStackTrace();
- return false;
- }
- }
- public static void main(String[] args) {
- String file1 = "D:\\develop\\projects\\dataStructure\\homework02\\src\\Homework0102\\list_result.txt";
- String file2 = "D:\\develop\\projects\\dataStructure\\homework02\\src\\Homework0102\\output.txt";
- boolean areEqual = compareFiles(file1, file2);
- if (areEqual) {
- System.out.println("两个文件的内容相同");
- } else {
- System.out.println("两个文件的内容不同");
- }
- }
- }
- 作图类Draw:
- package Homework0102;
- import org.jfree.chart.ChartFactory;
- import org.jfree.chart.ChartPanel;
- import org.jfree.chart.JFreeChart;
- import org.jfree.chart.plot.PlotOrientation;
- import org.jfree.chart.plot.XYPlot;
- import org.jfree.chart.renderer.xy.XYLineAndShapeRenderer;
- import org.jfree.chart.ui.ApplicationFrame;
- import org.jfree.chart.ui.RectangleInsets;
- import org.jfree.data.xy.XYDataset;
- import org.jfree.data.xy.XYSeries;
- import org.jfree.data.xy.XYSeriesCollection;
- import java.awt.*;
- import java.io.*;
- public class Draw extends ApplicationFrame {
- static DynamicArray<Double> UsageOfArray = new DynamicArray<>();
- static DynamicArray<Double> UsageOfDynamicArray = new DynamicArray<>();
- static double[] arrayUsageOfArray;
- static double[] arrayUsageOfDynamicArray;
- static double[] re;
- // 该构造方法中完成了数据集、图表对象和显示图表面板的创建工作
- public Draw(String title) {
- super(title);
- XYDataset dataset = createDataset(); // 创建记录图中坐标点的数据集
- JFreeChart chart = createChart(dataset); // 使用上一步已经创建好的数据集生成一个图表对象
- ChartPanel chartPanel = new ChartPanel(chart); // 将上一步已经创建好的图表对象放置到一个可以显示的Panel上
- // 设置GUI面板Panel的显示大小
- chartPanel.setPreferredSize(new Dimension(500, 270));
- setContentPane(chartPanel); // 这是JavaGUI的步骤之一,不用过于关心,面向对象课程综合训练的视频中进行了讲解。
- }
- private JFreeChart createChart(XYDataset dataset) {
- // 使用已经创建好的dataset生成图表对象
- // JFreechart提供了多种类型的图表对象,本次实验是需要使用XYLine型的图表对象
- JFreeChart chart = ChartFactory.createXYLineChart(
- "Space usage of the array",// 图表的标题
- "order", // 横轴的标题名
- "Space usage of the array", // 纵轴的标题名
- dataset, // 图表对象中使用的数据集对象
- PlotOrientation.VERTICAL, // 图表显示的方向
- true, // 是否显示图例
- false, // 是否需要生成tooltips
- false // 是否需要生成urls
- );
- // 下面所做的工作都是可选操作,主要是为了调整图表显示的风格
- // 同学们不必在意下面的代码
- // 可以将下面的代码去掉对比一下显示的不同效果
- chart.setBackgroundPaint(Color.WHITE);
- XYPlot plot = (XYPlot) chart.getPlot();
- plot.setBackgroundPaint(Color.lightGray);
- plot.setAxisOffset(new RectangleInsets(5.0, 5.0, 5.0, 6.0));
- plot.setDomainGridlinePaint(Color.WHITE);
- plot.setRangeGridlinePaint(Color.WHITE);
- XYLineAndShapeRenderer renderer = (XYLineAndShapeRenderer) plot.getRenderer();
- renderer.setDefaultShapesVisible(true);
- renderer.setDefaultShapesFilled(true);
- return chart;
- }
- private XYDataset createDataset() {
- double[][] Y = {arrayUsageOfArray, arrayUsageOfDynamicArray};
- XYSeries[] series = {new XYSeries("array"), new XYSeries("dynamic array")};
- int N = re.length;
- int M = series.length;
- System.out.println(M);
- System.out.println(N);
- for (int i = 0; i < M; i++)
- for (int j = 0; j < N; j++)
- series[i].add(re[j], Y[i][j]);
- // 因为在该图表中显示的数据序列不止一组,所以在jfreechart中需要将多组数据序列存放到一个XYSeriesCollection对象中
- XYSeriesCollection dataset = new XYSeriesCollection();
- for (int i = 0; i < M; i++)
- dataset.addSeries(series[i]);
- return dataset;
- }
- public static void main(String[] args) throws IOException, ListException {
- DynamicArray<Object> dynamicArray = new DynamicArray<>();
- Array<Object> array = new Array<>();
- FileReader fis = new FileReader("D:\\develop\\projects\\dataStructure\\homework02\\src\\Homework0102\\list_testcase.txt");
- BufferedReader br = new BufferedReader(fis);
- PrintWriter writer = new PrintWriter(System.out); // 创建PrintWriter对象
- String line;
- while ((line = br.readLine()) != null) {
- executeCommand(array, line);
- executeCommand(dynamicArray, line, writer);
- UsageOfArray.insert(array.SpaceUsageOfArray());
- UsageOfDynamicArray.insert(dynamicArray.SpaceUsageOfArray());
- writer.flush(); // 刷新PrintWriter的缓冲区
- }
- int numberOfLine = UsageOfDynamicArray.getCount();
- re = new double[numberOfLine];//一共有125行横坐标有125个
- arrayUsageOfArray = new double[numberOfLine];
- arrayUsageOfDynamicArray = new double[numberOfLine];
- for (int i = 0; i < re.length; i++) {
- re[i] = i + 1;
- }
- UsageOfArray.gotoBeginning();
- for (int i = 0; i < arrayUsageOfArray.length; i++) {
- arrayUsageOfArray[i] = UsageOfArray.getCursor();
- UsageOfArray.gotoNext();
- }
- UsageOfDynamicArray.gotoBeginning();
- for (int i = 0; i < arrayUsageOfDynamicArray.length; i++) {
- arrayUsageOfDynamicArray[i] = UsageOfDynamicArray.getCursor();
- UsageOfDynamicArray.gotoNext();
- }
- writer.close(); // 在循环结束后关闭PrintWriter对象
- Draw demo = new Draw("UsageOfArray");
- demo.pack();
- demo.setVisible(true);
- }
- private static void executeCommand(Array<Object> array, String line) throws ListException, IOException {
- StreamTokenizer st = new StreamTokenizer(new StringReader(line));
- int token = st.nextToken();
- while (token != StreamTokenizer.TT_EOF) {
- char command = (char) st.ttype;
- switch (command) {
- case '+' -> {
- st.nextToken();
- array.insert(st.sval);
- }
- case '-' -> array.remove();
- case '=' -> {
- st.nextToken();
- array.replace(st.sval);
- }
- case '#' -> array.gotoBeginning();
- case '*' -> array.gotoEnd();
- case '>' -> array.gotoNext();
- case '<' -> array.gotoPrev();
- case '~' -> array.clear();
- }
- token = st.nextToken();
- }
- }
- private static void executeCommand(DynamicArray<Object> array, String line, PrintWriter writer) throws ListException, IOException {
- StreamTokenizer st = new StreamTokenizer(new StringReader(line));
- int token = st.nextToken();
- while (token != StreamTokenizer.TT_EOF) {
- char command = (char) st.ttype;
- switch (command) {
- case '+' -> {
- st.nextToken();
- array.insert(st.sval);
- }
- case '-' -> array.remove();
- case '=' -> {
- st.nextToken();
- array.replace(st.sval);
- }
- case '#' -> array.gotoBeginning();
- case '*' -> array.gotoEnd();
- case '>' -> array.gotoNext();
- case '<' -> array.gotoPrev();
- case '~' -> array.clear();
- }
- token = st.nextToken();
- }
- }
- }