数据结构学习总结(二)线性表

舒适的环境很难培养出坚强的品格,被安排好的人生, 也很难做出伟大的事。

前言

线性结构是最简单,也是最常用的数据结构之一。线性结构的特点是:在数据元素的有限集中,除第一个元素无直接前驱,最后一个元素无直接后继以外,每个数据元素有且仅有一个直接前驱元素和一个直接后继元素。本中主要介绍了线性表的基本概念、定义线性表的抽象数据类型;在给出线性表的顺序存储结构和链式存储结构的基础上,分别给出线性表抽象数据类型的实现。

1. 线性表及抽象数据类型

线性表

1.1 线性表的定义

线性表:零个或多个具有相同数据类型的数据元素的有限序列

线性表的特点:
(1)元素个数有限 (2)逻辑上元素有先后次序
(3)数据类型相同 (4)仅讨论元素间的逻辑关系

注:线性表是逻辑结构,顺序表和链表是存储结构。

1.2 线性表的抽象数据类型

下面我们给出线性表的抽象数据类型定义。

ADT 线性表(List)
数据对象
	D = {ai | ai∈D0, i=0, 1, 2 … n-1,D0为某一数据对象}
数据关系:
	R = {<ai, ai+1> | ai, ai+1∈D,i=0, 1, 2 … n-2}
基本操作
	getSzie()		返回线性表的大小,即数据元素的个数。
	isEmpty()		如果线性表为空返回 true,否则返回 false。
	indexOf(e)		返回数据元素 e 在线性表中的序号。如果 e 不存在则返回-1。
	insert(i , e)	将数据元素 e 插入到线性表中 i 号位置。若 i 越界,报错。
	remove(i)		删除线性表中序号为 i 的元素,并返回之。若 i 越界,报错。
	replace(i, e)	替换线性表中序号为 i 的数据元素为 e,返回原数据元素。若 i 越界,报错。
	getElement(i)	返回线性表中序号为 i 的数据元素。若 i 越界,报错。
end ADT

在上述抽象数据类型的定义中,定义了几种基本操作,然而对于线性表的操作并不仅限于上述的操作,根据实际情况的需要还可以定义更多更复杂的操作。例如,将两个线性表合并为一个更大的线性表;把一个线性表分成两个线性表;对现有线性表进行复制等。

2. 顺序存储结构

线性表的顺序存储结构,指的是用一段地址连续的存储单元依次存储线性表的数据元素。 可以使用一维数组来实现顺序存储结构,数组大小有两种方式指定,一是静态分配,二是动态扩展。

线性表从 1 开始,而数组从 0 开始, 于是线性表的第 i 个元素是要存储在数组下标为 i-1 的位置,即数据元素的序号和存放它的数组下标之间存在对应关系,如下图所示。

内存地址

假设线性表的每个数据元素需占用 K 个存储单元,则线性表中第 i 个数据元素的存储地址 LOC(ai) 与第 i+1 个数据元素的存储地址 LOC(ai+1) 之间的关系为(LOC 表示获得存储地址的函数):
LOC(ai+1) = LOC(ai) + K
所以对于第 i 个数据元素的存储地址可以由 a1 推算得出:
LOC(ai) = LOC(a1) + (i-1)*K
其中 LOC(a1) 为第一个元素 a1 的存储地址,通常称为线性表的起始地址。

线性表顺序存储结构 Java 实现:

public class ArrayList<T> {
	private final int MAXSIZE = 8; // 数组的默认大小
	private int size; // 线性表中数据元素的个数
	private T[] elements; // 数据元素数组

	// 构造方法
	public ArrayList() {
		this.size = 0;
		this.elements = (T[]) new Object[MAXSIZE];
	}

	// 返回线性表的大小,即数据元素的个数
	public int getSize() {
		return this.size;
	}

	// 如果线性表为空返回 true,否则返回 false
	public boolean isEmpty() {
		return this.size == 0;
	}

	public void checkIndex(int index) {
		if (index < 1 || index > size + 1) {
			throw new IndexOutOfBoundsException("位置" + index + "越界");
		}
	}

	// 将数据元素e插入到线性表中第i个位置
	public void insert(int i, T e) {
		checkIndex(i);

		if (size >= elements.length) {
			expandSpace();
		}
		for (int j = size; j > i - 1; j--) {
			elements[j] = elements[j - 1];
		}
		elements[i - 1] = e;
		size++;
	}

	// 数组扩容
	private void expandSpace() {
		T[] a = (T[]) new Object[elements.length * 2];
		for (int i = 0; i < elements.length; i++) {
			a[i] = elements[i];
		}
		elements = a;
	}

	// 删除线性表的第i个元素,并返回之
	public Object remove(int i) {
		checkIndex(i);

		Object obj = elements[i - 1];
		for (int j = i - 1; j < size - 1; j++) {
			elements[j] = elements[j + 1];
		}
		elements[--size] = null;
		return obj;
	}

	// 将线性表的第i个元素替换为e,返回原数据元素
	public void replace(int i, T e) {
		checkIndex(i);

		elements[i - 1] = e;
	}

	// 获取线性表第i个元素
	public T getElement(int i) {
		checkIndex(i);

		return elements[i - 1];
	}

	// 返回数据元素e在线性表中的位置
	public int indexOf(T e) {
		for (int i = 0; i < size; i++) {
			if (elements[i].equals(e)) {
				return i + 1;
			}
		}
		return -1;
	}
}

顺序存储结构优缺点

优点
随机访问特性,查找 O(1) 时间,存储密度高;逻辑上相邻的元素,物理上也相邻;
缺点
插入删除需移动大量元素

3. 链式存储结构

实现线性表的另一种方法是链式存储,即用指针将存储线性表中数据元素的那些单元依次串联在一起。这种方法避免了在数组中用连续的单元存储元素的缺点,因而在执行插入或删除运算时,不再需要移动元素来腾出空间或填补空缺。然而我们为此付出的代价是,需要在每个单元中设置指针来表示表中元素之间的逻辑关系,因而增加了额外的存储空间的开销。

链表是一系列的存储数据元素的单元通过指针串接起来形成的,因此每个单元至少有两个域,一个域用于数据元素的存储,另一个域是指向其他单元的指针。这里具有一个数据域和多个指针域的存储单元通常称为结点(node)。

3.1 单链表

如下图所示,单链表由 n 个节点链接而成,且每个结点只包含一个指针域
单链表

链表中第一个结点的存储位置叫做头指针。
单链表分为带头结点和不带头结点两种,不管有没有头结点,头指针都指向链表的第一个节点(有头结点指向头结点)。

头结点:数值域可不设任何信息,头结点的指针域指向链表的第一个元素。

带头节点的好处有:
(1)链表第一位置节点上的操作和其它位置上的操作一致
(2)无论链表是否为空,头指针都指向头结点(非空),空表和非空表处理一样

带头结点的头指针

Java 中是使用对象的引用来替代指针的。

单链表的插入与删除

插入节点
待插入节点为 s,一般采用后插法,即先找到插入位置节点的前驱节点,然后插入,时间复杂度O(n)。
在这里插入图片描述
还有一种方法是,直接插入到指定位置的后面(前插法),然后交换两个节点的值。

删除节点
待删除节点为q,也是先找到前驱节点,修改指针域即可,时间复杂度O(n)。
在这里插入图片描述
删除节点也能直接删除其后继节点,然后将后继节点的内容赋给自己即可,时间复杂度为O(1)。

单链表 Java 实现

public class SingleLinkedList<T> {
	private Node<T> head; // 头结点:指向第一个结点的位置
	private Node<T> tail; // 尾结点
	private int size; // 链表中元素个数

	public SingleLinkedList() {
		this.head = new Node<T>();
		this.tail = new Node<T>();
		head.next = tail.next = null;
	}

	// 获取元素个数
	public int size() {
		return this.size;
	}

	// 检查是否越界
	private void checkIndex(int index) {
		if (index < 0 || index > size)
			throw new IndexOutOfBoundsException("Index: " + index + ", Size: " + size);
	}

	// 获取指定位置的结点
	private Node<T> entry(int index) {
		checkIndex(index);

		Node<T> node = head.next;
		for (int i = 0; i < index; i++) {
			node = node.next;
		}

		return node;
	}

	// 在链表末尾添加元素
	public boolean add(T o) {
		return add(size, o);
	}

	// 在指定位置插入元素(前插法)
	public boolean add(int index, T o) {
		checkIndex(index);

		Node<T> node = new Node<T>(o, null);

		Node<T> prev; // 获取待插入位置的前一个结点
		if (index == 0) {
			prev = head;
		} else {
			prev = entry(index - 1);
		}
		node.next = prev.next;
		prev.next = node;

		if (index == size) {
			this.tail = node;
		}

		size++;
		return true;
	}

	// 获取第i个元素
	public T getElem(int index) {
		if (index < 0 || index >= size) {
			throw new IndexOutOfBoundsException("Index: " + index + ", Size: " + size);
		}

		return entry(index).data;
	}

	// 删除指定位置结点,并返回该结点元素
	public T remove(int index) {
		if (index < 0 || index >= size) {
			throw new IndexOutOfBoundsException("Index: " + index + ", Size: " + size);
		}

		Node<T> prev = entry(index - 1);
		Node<T> target = prev.next;

		prev.next = target.next;
		target.next = null;
		T t = target.data;
		target.data = null;

		if (index == size - 1) {
			this.tail = prev;
		}

		size--;
		return t;
	}

	// 结点定义
	private static class Node<T> {
		T data;
		Node<T> next;

		Node(T t, Node<T> next) {
			this.data = t;
			this.next = next;
		}

		Node() {
		}
	}
}

经验总结:

  • 若线性表需要频繁查找,很少进行插入和删除操作时,宜采用顺序存储结构。若需要频繁插入和删除时,宜采用单链表结构。
  • 当线性表中的元素个数变化较大或者根本不知道有多大时,最好用单链表结构,这样可以不需要考虑存储空间的大小问题。

3.2 静态链表

C 语言具有指针的能力,Java、C# 等虽不使用指针,但因启用了对象引用机制,从某种角度也间接实现了指针的某些作用。但对于一些语言,如 Basic、Fortran 等早期的编程高级语言,由于没有指针,该如何实现链表呢?

答案就是用数组代替指针,来描述单链表。首先让数组的元素都是由两个数据域组成,data 和 cur。也就是说,数组的每个下标都对应一个 data 和一个 cur。数据域 data,用来存放数据元素;而 cur(游标) 相当于单链表中的 next 指针,这里和链表不同是,它存的不再是指向下一个结点的内存地址。而是下一个节点在数组中的下标。这种用数组描述的链表叫做静态链表,也叫游标实现法。
静态链表
由上图我们需要注意以下几点:

  • 我们对数组的第一个元素和最后一个元素做特殊处理,不存放数据。
  • 把未使用的数组元素称为备用链表。
  • 数组的第一个元素(下标为0)的 cur 域存放备用链表第一个结点的下标。
  • 数组的最后一个元素的 cur 域存放第一个有数据的结点的下标,相当于链表中头结点的存在。链表为空时,其值为0。

引出的问题:数组的长度定义的问题,无法预支。所以,为了防止溢出,我们一般将静态表开的大一点。

静态表 Java 实现

3.3 循环链表

将单链表的尾结点的指针由空指针改为指向头结点,就使整个单链表形成一个环。这种头尾相接的单链表称为单循环链表,简称循环链表。
循环链表

3.4 双向链表

双向链表是在单链表的每个结点中,再设置一个指向其前驱结点的指针域。 所以在双向链表的每个结点中都有两个指针域,一个指向直接后继,另一个指向直接前驱。
双向链表

  • 1
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值