数据结构与算法(三)线性表
线性表定义
线性表( List ):零个或多个数据元素的有限序列。
首先它是一个 序列 ,其次,线性表强调是 有限的 。
前驱元素 :若A元素在B元素的前面,则称A为B的前驱元素。
后继元素 :若B元素在A元素的后面,则称B为A的后继元素。 线性表的特征
数据元素之间具有一种 一对一 的逻辑关系。 第一个数据元素没有前驱,这个数据元素被称为 头结点 ; 最后一个数据元素没有后继,这个数据元素被称为 尾结点 ; 除了第一个和最后一个数据元素外,其他数据元素有且仅有一个前驱和一个后继。
如果把线性表用数学语言来定义,则可以表示为( a1,...ai-1,ai,ai+1,...an ), ai-1 领先于 ai , ai 领先于 ai+1 ,称 ai-1 是 ai 的前驱元素, ai+1 是 ai 的后继元素。
线性表的分类
线性表中数据存储的方式可以是 顺序存储 ,也可以是 链式存储 ,按照数据的存储方式不同,可以把线性表分为顺序表和链表。 顺序表
顺序表是在计算机内存中以数组的形式保存的线性表,线性表的顺序存储是指用 一组地址连续的存储单元 ,依次存储线性表中的各个元素、使得线性表中再逻辑结构上相邻的数据元素存储在相邻的物理存储单元中,即通过数据元素物理存储的相邻关系来反映数据元素之间逻辑上的相邻关系。
顺序表设计
类名
SequenceList
构造方法
SequenceList(int capacity) :创建容量为 capacity 的 SequenceList 对象
成员方法 public void clear() :空置线性表
publicboolean isEmpty() :判断线性表是否为空,是返回true,否返回false
public int length() :获取线性表中元素的个数
public T get(int i) :读取并返回线性表中的第i个元素的值
public void insert(int i, E e) :在线性表的第i个元素之前插入一个值为e的数据元素。
public void insert(E e) :向线性表中添加一个元素e
public T remove(int i) :删除并返回线性表中第i个数据元素。
public int indexOf(E e) :返回线性表中首次出现的指定的数据元素的位序号,若不存在,则返回-1。
成员变量 private T[] elements :存储元素的数组
private int n :当前线性表的长度顺序表代码实现@SuppressWarnings("unchecked") public class SequenceList implements Iterable { /** * 存储元素的数组 */ private T[] elements; /** * 记录当前顺序表中的元素个数 */ private int n; /** * 创建容量为capacity的SequenceList对象 */ public SequenceList(int capacity) { // 初始化数组 this.elements = (T[]) new Object[capacity]; // 初始化长度 n = 0; } /** * 空置线性表 */ public void clear() { n = 0; } /** * 判断线性表是否为空,是返回true,否返回false */ public boolean isEmpty() { return n == 0; } /** * 获取线性表中元素的个数 */ public int length() { return n; } /** * 读取并返回线性表中的第i个元素的值 */ public T get(int i) { if (i < 0 || i >= n) { throw new RuntimeException("当前元素不存在!"); } return elements[i]; } /** * 在线性表的第i个元素之前插入一个值为t的数据元素 */ public void insert(int i, E e) { if (n == elements.length) { throw new RuntimeException("当前表已满"); } // 先把i索引处的元素及其后面的元素依次向后移动一位 for (int j = n; j > i; j--) { elements[j] = elements[j - 1]; } // 再把t元素放到i索引处 elements[i] = t; n++; } /** * 向线性表中添加一个元素t */ public void insert(E e) { if (i == elements.length) { throw new RuntimeException("当前表已满"); } if (i < 0 || i > n) { throw new RuntimeException("插入的位置不合法"); } elements[n++] = t; } /** * 删除并返回线性表中第i个数据元素 */ public T remove(int i) { if (i < 0 || i > n - 1) { throw new RuntimeException("当前要删除的元素不存在"); } // 记录i索引处的值 T current = elements[i]; // 索引i后面元素依次向前移动一位 for (int j = i; j < n - 1; j++) { elements[j] = elements[j + 1]; } // 元素个数-1 n--; return current; } /** * 返回线性表中首次出现的指定的数据元素的位序号,若不存在,则返回-1。 */ public int indexOf(E e) { if (t == null) { throw new RuntimeException("查找的元素不合法"); } for (int i = 0; i < n; i++) { if (Objects.equals(elements[i], t)) { return i; } } return -1; } }顺序表的遍历
一般作为容器存储数据,都需要向外部提供遍历的方式,因此我们需要给顺序表提供遍历方式。
在Java中,遍历集合的方式一般都是用的是 forEach 循环,如果想让我们的 SequenceList 也能支持 forEach 循环,则需要做如下操作: 让 SequenceList 实现 Iterable 接口,重写 iterator 方法; 在 SequenceList 内部提供一个内部类 SIterator ,实现 Iterator 接口,重写 hasNext 方法和 next 方法; public class SequenceList implements Iterable { // ... @Override public Iterator iterator() { return new SIterator(); } private class SIterator implements Iterator { private int cursor; public SIterator() { this.cursor = 0; } @Override public boolean hasNext() { return cursor < n; } @Override public T next() { return elements[cursor++]; } } }顺序表容量可变
在之前的实现中,当我们使用 SequenceList 时,先 new SequenceList(5) 创建一个对象,创建对象时就需要指定容器的大小,初始化指定大小的数组来存储元素,当我们插入元素时,如果已经插入了5个元素,还要继续插入数据,则会报错,就不能插入了。这种设计不符合容器的设计理念,因此我们在设计顺序表时,应该考虑它的容量的伸缩性。
考虑容器的容量伸缩性,其实就是 改变存储数据元素的数组的大小 ,那我们需要考虑什么时候需要改变数组的大小? 添加元素时扩容
添加元素时,应该检查当前数组的大小是否能容纳新的元素,如果不能容纳,则需要创建新的容量更大的数组,我们这里创建一个是原数组两倍容量的新数组存储元素。
移除元素时缩容
移除元素时,应该检查当前数组的大小是否太大,比如正在用 100 个容量的数组存储 10 个元素,这样就会造成内存空间的浪费,应该创建一个容量更小的数组存储元素。如果我们发现数据元素的数量不足数组容量的 1/4 ,则创建一个是原数组容量的 1/2 的新数组存储元素。
@SuppressWarnings("unchecked") public class SequenceList { /** * 向线性表中添加一个元素t */ public void insert(E e) { // 如果当前容量已满,那么扩容2倍 if (n == elements.length) { resize(2 * elements.length); } // ... } /** * 删除并返回线性表中第i个数据元素 */ public T remove(int i) { // ... // 如果当前元素数量小于容量的1/4,那么缩容为1/2 if (n < elements.length / 4) { resize(elements.length / 2); } return current; } /** * 根据newSize,重置elements的大小 */ public void resize(int newSize) { // 定义一个临时数组,指向原数组 T[] temp = elements; // 创建新数组 elements = (T[]) new Object[newSize]; System.arraycopy(temp, 0, elements, 0, temp.length); } }
扩缩容的原理很简单,是创建一个具有指定新容量的新数组,然后把原来的数据拷贝到新数组。 顺序表的时间复杂度get(i) :不论数据元素量 n 有多大,只需要一次 elements[i] 就可以获取到对应的元素,所以时间复杂度为 O(1) 。我们通常把具有这一特点的存储结构称为随机存取结构。 insert(int i,E e) :每一次插入,都需要把i位置后面的元素移动一次,随着元素数量N的增大,移动的元素也越多,时间复杂为 O(n) ; remove(int i) :每一次删除,都需要把i位置后面的元素移动一次,随着数据量N的增大,移动的元素也越多,时间复杂度为 O(n) ;
由于顺序表的底层由数组实现,数组的长度是固定的,所以在操作的过程中涉及到了容器扩容操作。这样会导致顺序表在使用过程中的时间复杂度不是线性的,在某些需要扩容的结点处,耗时会突增,尤其是元素越多,这个问题越明显。 顺序表的优缺点优点无需为表示表中元素之间的逻辑关系而增加额外的存储空间; 可以快速地存取表中任意位置的元素。 缺点插入和删除操作需要移动大量元素; 当线性表长度变化较大时,难以确定存储空间的容量; 造成存储空间的 碎片 。 链表
虽然顺序表的查询很快,时间复杂度为 O(1) ,但是 增删的效率是比较低的 ,因为每一次增删操作都伴随着大量的 数据元素移动 。这个问题有没有解决方案呢?
有,我们可以使用另外一种存储结构实现线性表,链式存储结构。
链表是一种物理存储单元上非连续、非顺序的存储结构,其物理结构不能只管的表示数据元素的逻辑顺序,数据元素的逻辑顺序是通过链表中的指针链接次序实现的。
链表由一系列的结点(链表中的每一个元素称为结点)组成,结点可以在运行时动态生成。
链表节点设计
类名
Node
构造方法
Node(E e, Node next) :创建 Node 对象
成员变量 E item :存储数据
Node next :指向下一个结点@NoArgsConstructor @AllArgsConstructor @Data public class Node { /** * 存储元素 */ private E item; /** * 指向下一个节点 */ private Node next; }单向链表
单向链表是链表的一种,它由多个结点组成,每个结点都由一个数据域和一个指针域组成,数据域用来存储数据,指针域用来指向其后继结点。链表的头结点的数据域不存储数据,指针域指向第一个真正存储数据的结点。
单向链表设计
类名
LinkList
构造方法
LinkList() :创建 LinkList 对象
成员方法 public void clear() :空置线性表
public boolean isEmpty() :判断线性表是否为空,是返回true,否返回false
public int length() :获取线性表中元素的个数
public E get(int i) :读取并返回线性表中的第i个元素的值
public void insert(E e) :往线性表中添加一个元素;
public void insert(int i, E e) :在线性表的第i个元素之前插入一个值为t的数据元素。
public E remove(int i) :删除并返回线性表中第i个数据元素。
public int indexOf(E e) :返回线性表中首次出现的指定的数据元素的位序号,若不存在,则返回-1。
成员变量 private Node head :记录首结点
private int n :记录链表的长度单向链表代码实现public class LinkList implements Iterable { /** * 记录首节点 */ private final Node head; /** * 记录链表的长度 */ private int n; public LinkList() { // 初始化头结点 head = new Node<>(null, null); // 初始化元素个数 n = 0; } /** * 空置线性表 */ public void clear() { head.next = null; n = 0; } /** * 判断线性表是否为空,是返回true,否返回false */ public boolean isEmpty() { return n == 0; } /** * 获取线性表中元素的个数 */ public int length() { return n; } /** * 读取并返回线性表中的第i个元素的值 */ public E get(int i) { if (i < 0 || i >= n) { throw new RuntimeException("位置不合法!"); } // 通过循环,从头结点开始往后找,依次找i次,就可以找到对应的元素 Node n = head.next; for (int index = 0; index < i; index++) { n = n.next; } return n.item; } /** * 往线性表中添加一个元素 */ public void insert(E e) { // 找到当前最后一个节点 Node n = head; // 头节点不存储数据,所以不能算作第一个元素 while (n.next != null) { n = n.next; } // 创建新节点,保存元素t // 让当前最后一个元素指向新节点 n.next = new Node<>(e, null); // 元素个数+1 this.n++; } /** * 在线性表的第i个元素之前插入一个值为t的数据元素 */ public void insert(int i, E e) { if (i < 0 || i >= n) { throw new RuntimeException("位置不合法!"); } // 找到i位置前一个节点 Node pre = head; // 头节点不存储数据,所以不能算作第一个元素 for (int index = 0; index < i; index++) { pre = pre.next; } // 找到i位置的节点 Node current = pre.next; // 创建新节点,并且新节点需要指向原来i位置的节点 // 原来i位置的前一个节点指向新节点 pre.next = new Node<>(e, current); // 元素个数+1 n++; } /** * 删除并返回线性表中第i个数据元素 */ public E remove(int i) { if (i < 0 || i >= n) { throw new RuntimeException("位置不合法"); } // 找到i位置前一个节点 Node pre = head; for (int index = 0; index < i; index++) { pre = pre.next; } // 找到i位置的节点 Node current = pre.next; // 找到i位置的下一个节点 // 前一个节点指向下一个节点 pre.next = current.next; // 元素个数 - 1 n--; return current.item; } /** * 返回线性表中首次出现的指定的数据元素的位序号,若不存在,则返回-1 */ public int indexOf(E e) { // 从头结点开始,依次找到每一个节点,取出item,和t比较,如果相同,就返回 Node n = head; for (int i = 0; n.next != null; i++) { n = n.next; if (n.item.equals(e)) { return i; } } return -1; } @Override public Iterator iterator() { return new LIterator(); } private static class Node { // 存储元素 T item; // 指向下一个节点 Node next; Node(T item, Node next) { this.item = item; this.next = next; } } }循环链表
对于单向链表,由于每个结点只存储了向后的指针,到了尾标志就停止了向后链的操作,这样,当中某一结点就无法找到它的前驱结点了。
比如,你是一业务员,家在上海,需要经常出差,行程就是上海到北京一路上的城市,找客户谈生意或分公司办理业务。你从上海出发,乘火车路经多个城市停留后,再乘飞机返回上海,以后,每隔一段时间,你基本还要按照这样的行程开展业务,如图所示:
有一次,你先到南京开会,接下来要对以上的城市走一遍,此时有人对你说,不行,你得从上海开始,因为上海是第一站。你会对这人说什么?神经病。哪有这么傻的,直接回上海根本没有必要,你可以从南京开始,下一站蚌埠,直到北京,之后再考虑走完上海及苏南的几个城市。
显然这表示你是从当中一结点开始遍历整个链表,这都是原来的单链表结构解决不了的问题。事实上,把北京和上海之间连起来,形成一个环就解决了前面所面临的困难。这就是循环链表。
从刚才的例子,可以总结出,循环链表解决了一个很麻烦的问题。 如何从当中一个结点出发,访问到链表的全部结点 。
在单向链表中,最后一个节点的指针为 NULL ,不指向任何结点,因为没有下一个元素了。要实现循环链表,我们只需要 让单向链表的最后一个节点的指针指向头结点 即可。
如果链表中没有元素,那么头结点也需要指向自己,从而形成环。
循环链表代码实现
代码实现和单向链表基本一致,只需要在插入尾结点的时候,将尾结点指向头结点即可。 public void insert(E e) { // 找到当前最后一个节点 // 头节点不存储数据,所以不能算作第一个元素 var n = head; while (n.next != null) { n = n.next; } // 创建新节点,保存元素t,让当前最后一个元素指向新节点 // 循环链表,最后一个元素指向头结点 n.next = new Node<>(e, head); // 元素个数+1 this.n++; }
同时,在构造和清空链表时,让头结点指向自己 public CycleLinkList() { // 初始化头结点 head = new Node<>(null, null); head.next = head; // 初始化元素个数 n = 0; } /** * 清空线性表 */ public void clear() { head.next = head; n = 0; }双向链表
继续刚才的例子,你平时都是从上海一路停留到北京的,可是这一次,你得先到北京开会,谁叫北京是首都呢,会就是多。开完会后,你需要例行公事,走访各个城市,此时你怎么办?
有人又出主意了,你可以先飞回上海,一路再乘火车走遍这几个城市,到了北京后,你再飞回上海。你会感慨,人生中为什么总会有这样出馊主意的人存在呢?真要气死人才行。哪来这么麻烦,我一路从北京坐火车或汽车回去不就完了吗。
我们的单链表,总是从头到尾找结点,难道就不可以正反遍历都可以吗?当然可以,只不过需要加点东西而已。
双向链表:在单链表的每个结点中,再设置一个指向其前驱结点的指针域。所以在双向链表中的结点都有 两个指针域 ,一个指向直接后继,另一个指向直接前驱。 双向链表结点设计
类名
Node
构造方法
Node(E e, Node pre, Node next) :创建Node对象
成员变量 E item :存储数据
Node next :指向下一个结点
Node pre :指向上一个结点@AllArgsConstructor private static class Node { // 存储元素 E item; // 指向上一个节点 Node pre; // 指向下一个节点 Node next; }双向链表设计
类名
DoubleLinkList
构造方法
DoubleLinkList() :创建 DoubleLinkList 对象
成员方法 public void clear() :空置线性表
public boolean isEmpty() :判断线性表是否为空,是返回true,否返回false
public int length() :获取线性表中元素的个数
public T get(int i) :读取并返回线性表中的第i个元素的值
public void insert(E e) :往线性表中添加一个元素;
public void insert(int i, E e) :在线性表的第i个元素之前插入一个值为t的数据元素。
public T remove(int i) :删除并返回线性表中第i个数据元素。
public int indexOf(E e) :返回线性表中首次出现的指定的数据元素的位序号,若不存在,则返回-1。
public T getFirst() :获取第一个元素
public T getLast() :获取最后一个元素
成员变量 private Node first :记录首结点
private Node last :记录尾结点
private int n :记录链表的长度双向链表代码实现public class DoubleLinkList implements Iterable { /** * 记录首节点 */ private final Node head; /** * 记录尾节点 */ private Node last; /** * 记录链表的长度 */ private int n; public DoubleLinkList() { // 初始化头结点 head = new Node<>(null, null, null); // 初始化尾节点 last = null; // 初始化元素个数 n = 0; } /** * 空置线性表 */ public void clear() { head.next = null; last = null; n = 0; } /** * 判断线性表是否为空,是返回true,否返回false */ public boolean isEmpty() { return n == 0; } /** * 获取线性表中元素的个数 */ public int length() { return n; } /** * 获取头结点 */ public E getFirst() { if (isEmpty()) { return null; } return head.next.item; } /** * 获取尾节点 */ public E getLast() { if (isEmpty()) { return null; } return last.item; } /** * 读取并返回线性表中的第i个元素的值 */ public E get(int i) { if (i < 0 || i >= n) { throw new RuntimeException("位置不合法!"); } // 通过循环,从头结点开始往后找,依次找i次,就可以找到对应的元素 Node n = head.next; for (int index = 0; index < i; index++) { n = n.next; } return n.item; } /** * 往线性表中添加一个元素 */ public void insert(E e) { if (isEmpty()) { // 如果链表为空 // 创建新的节点 Node newNode = new Node<>(e, head, null); // 让新节点成为尾节点 last = newNode; // 让头结点指向尾节点 head.next = last; } else { // 如果链表不为空 Node tempLast = last; // 创建新的节点 Node newNode = new Node<>(e, tempLast, null); // 当前的尾节点指向新节点 tempLast.next = newNode; // 让新节点成为尾节点 last = newNode; } // 元素个数+1 n++; } /** * 在线性表的第i个元素之前插入一个值为t的数据元素 */ public void insert(int i, E e) { if (i < 0 || i >= n) { throw new RuntimeException("位置不合法!"); } // 找到i位置的前一个节点 Node pre = head; for (int index = 0; index < i; index++) { pre = pre.next; } // 找到i位置的节点 Node current = pre.next; // 创建新节点 Node newNode = new Node<>(e, pre, current); // 让i位置的前一个节点指向新节点 pre.next = newNode; // 让i位置的前一个节点变为新节点 current.pre = newNode; // 元素个数+1 n++; } /** * 删除并返回线性表中第i个数据元素 */ public E remove(int i) { if (i < 0 || i >= n) { throw new RuntimeException("位置不合法"); } // 找到i位置前一个节点 Node pre = head; for (int index = 0; index < i; index++) { pre = pre.next; } // 找到i位置的节点 Node current = pre.next; // 找到i位置的下一个节点 Node next = current.next; // i位置的前一个节点的下一个节点指向i位置的下一个节点 pre.next = next; // i位置的下一个节点的前一个节点指向i位置的前一个节点 next.pre = pre; // 元素个数 - 1 n--; return current.item; } /** * 返回线性表中首次出现的指定的数据元素的位序号,若不存在,则返回-1 */ public int indexOf(E e) { // 从头结点开始,依次找到每一个节点,取出item,和t比较,如果相同,就返回 Node n = head; for (int i = 0; n.next != null; i++) { n = n.next; if (n.item.equals(e)) { return i; } } return -1; } @Override public Iterator iterator() { return new LIterator(); } @AllArgsConstructor private static class Node { // 存储元素 E item; // 指向上一个节点 Node pre; // 指向下一个节点 Node next; } private class LIterator implements Iterator { private Node n = head; @Override public boolean hasNext() { return n.next != null; } @Override public E next() { n = n.next; return n.item; } } }链表的时间复杂度get(int i) :每一次查询,都需要从链表的头部开始,依次向后查找,随着数据元素N的增多,比较的元素越多,时间复杂度为 O(n) ; insert(int i, E e) :每一次插入,需要先找到i位置的前一个元素,然后完成插入操作,随着数据元素N的增多,查找的元素越多,时间复杂度为 O(n) ; remove(int i) :每一次移除,需要先找到i位置的前一个元素,然后完成插入操作,随着数据元素N的增多,查找的元素越多,时间复杂度为 O(n) 。
相比较顺序表,链表插入和删除的时间复杂度虽然一样,但仍然有很大的优势,因为链表的物理地址是不连续的,它 不需要预先指定存储空间大小 ,并且在存储过程中 不涉及到扩容等操作 ,同时它并没有涉及的元素的交换。
相比较顺序表,链表的查询操作性能会比较低。
因此,如果我们的程序中 查询操作比较多,建议使用顺序表,增删操作比较多,建议使用链表 。
鞠婧祎只要突出我的美貌就行了,不必在意细节俗话说,女大十八变。女孩子到了十八岁后,无论是在外貌上还是在其他方面,都会发生很大的变化。鞠婧祎早期照有四千年一遇美女称号的鞠婧祎,因为在模样上有很大的差别,被网友们调侃为四千刀美
20220805国产芯片概念解读名称流通市值(2285)国产芯片概念解读SZ002594比亚迪3779。54亿元公司已经陆续掌握IGBT芯片设计和制造模组设计和制造大功率器件测试应用平台电源及电控等环节,是中国唯
立秋吃3宝,不把医生找,今日立秋,3宝是啥?咋做最好吃?立秋吃3宝,不把医生找,今日立秋,3宝指的是啥?咋做最好吃?哈喽,大家好。我是大厨江一舟。今天又到了和大家分享美食的时刻了,你准备好了吗?今天就是立秋节气啦,细心的朋友会发现。空气
这次环岛军演的几大非典型意外我们正在进行的环岛军事演训震惊了世界,也可以说让很多人颇感意外1按8月2日的新华社公告,环岛联合军事演训是到8月7日12时结束,可是意外的是东部战区8月7日12点多发布消息,继续进
全国最大的二手手机批发市场倒闭的原因最大的二手手机批发市场,档口几乎全在转让,这到底是怎么回事?华强北的柜台,如果在以前光转租的中介费都是几万块钱,现在却全部免费转让,甚至还给你免租一个月,这是闹哪样?,华强北难道真
怼!航海重新起航中超第二阶段第一场(第11轮),河南嵩山龙门时隔980天重新坐镇航海主场迎战北京国安。或许,那专治各种不服的标语已经不足以表达河南球迷的心声,或许球迷们胸前那一个简简单单的怼字才能
台独的前途在哪里1997年7月1日香港回到祖国的怀抱,一国两制从此展现无限生机,此时香港人均GDP2。73万美元。1999年12月20日澳门顺利回归,人均GDP1。55万美元。19951996年台
得道多助失道寡助打卡挑战局当年那样威风凛凛的秦帝国为啥二世而亡,短时间就土崩瓦解了?我认为主要原因还是秦帝国失去了民心的支持。第一,搜刮各国财富,奴役各国民众。把各国的珍器重宝都弄到秦帝国宫殿。让
一个父亲的角色昨晚是周五,下班时接到李强的电话,说几个哥们好久没聚了,今晚他做东聚一聚。因为疫情的影响,哥们都有好几个月没见面了,饭桌上大家都很开心。喝了几杯后更加激动了,很晚都不舍得散去!到了
无论是国民党的李登辉还是民进党的蔡英文为什么都亲日闹台独呢?不知道大家看过台湾人拍的反抗日本统治的史诗巨片赛德克巴莱吗?是的,台湾曾经被日本统治了50年,在这五十年里有一部分台湾人已经被彻底奴化了,他们信奉武力,屈服于强者,喜欢抱美国人的大
一个70后闯深圳的故事1992到1993,这是我大学毕业的一年,按理是开心的,可是于我是极不平凡的一年。四年的大学结束了,同学都没黑没明地喝酒,有时一个人一天喝十几瓶啤酒,整天浑浑噩噩。大多数同学都私下