初入数据结构中的线性表(Linear table)及Java代码实现
因为博主目前主要学习的语言是Java,所以对于数据结构方面的实现,也是Java。
- 线性表的概念
- 什么是线性表?
- 前趋和后继
- 数据元素、数据项、记录和文件
- 线性表的特点
- 线性表的分类
- 顺序表的概念
- 什么是顺序表?
- 顺序表的代码实现
链表的概念
- 什么是链表
- 链表的特点
- 链表中元素的构成
- 头结点、头指针和首元结点
- 链表的代码实现
顺序表和单链表的优缺点比较
线性表的概念
什么是线性表?
线性表:
线性表就是数据结构中最简单的存储结构,可以理解为“线性的表”
线性:
什么是线性,就是说数据在逻辑结构上拥有线性关系。
线性关系:
那什么是线性关系,线性关系是指数据一个挨着一个,总体呈线性分布。
所以线性表就是一个逻辑上数据一个挨着一个,呈线性关系排列的简单数据结构
前趋和后继
我们刚刚了解了什么是线性表,那么现在,我们来了解一下线性表里面的一些小概念。什么是前趋?什么是后继?
前趋和后继:
对于线性表的数据来说,前趋后继是相对的,位于当前数据之前的数据统称为“前趋”,位于当前数据之后的数据统称为“后继”。
直接前趋和直接后继:
当前数据的前一个数据就是“直接前趋”,当前数据的后一个数据就是“直接后继”。
数据元素、数据项、记录和文件
在线性表中,无论数据本身由多少种数据类型组成,每一条数据都称为“数据元素”,其中的每一种称为一个“数据项”
如果数据元素本身包含非常多的数据项,就可以称这个数据元素为一个“记录”,多个记录组成一个“文件”
举个粟子:
从sql表中举例(仅仅是为了举例),一个表由多个字段,我们可以把每个字段当做是一个数据项。那么表里的一条完整数据,我们就称为一个数据元素,因为这个数据元素含有很多数据项,因为有很多列。所以一条数据元素,我们也称为一条记录。而数据库的一个表,肯定拥有很多条记录,我们就称这个表的所有记录组合成一个文件
线性表的特点
线性表存储的数据的几个特点:
- 数据一旦用线性表存储,就是有序的,各个数据元素之间的相对位置就固定了
- 线性表第一个元素有且仅有一个直接后继,最后一个元素有且仅有一个直接前趋
- 线性表允许零元素,既空表
线性表的分类
线性表中的数据在逻辑结构上拥有线性关系,相对在物理存储中有两种线性关系:
- 数据在内存中分配的空间是集中连续存储的,在物理结构中拥有线性关系,称为“顺序存储”
- 数据在内存中分配的空间是分散不连续存储的,非物理线性,而是在逻辑结构上拥有线性关系,称为“链式存储”
顺序表的概念
什么是顺序表?
顺序表:
什么是顺序表?采用顺序存储结构生成的表,称为顺序表
如上图,同一顺序表的数据在物理内存地址中是具有连续性的。
(数组就是顺序表的其中一个应用)
顺序表的代码实现
我这里的顺序表是一个不会自动扩容的顺序表,采用Java中的数组为底层结构。实现顺序表的初始化、查找、插入、修改、删除元素功能:
- 顺序表是否为空
- 顺序表的长度
- 查找顺序表指定索引位置的元素
- 修改顺序表指定索引位置的元素
- 顺序插入元素
- 插入顺序表指定索引位置的元素
- 删除顺序表指定索引位置的元素
import java.util.Arrays;
//这是一个容量不可变的顺序表
public class SeqList<T> {
private Object [] element; //数组
private int len; //顺序表长度
private int size; //顺序表容量
private SeqList(int size){ //顺序表的初始化,容量为size的空表
this.element = new Object[size];
this.size = size;
this.len = 0;
}
/***********************************查找********************************/
//判断顺序表是否为空
public boolean isEmpty(){
return this.len == 0;
}
//返回顺序表的长度
public int length(){
return this.len;
}
//查找第i个元素,并返回.若不存在则报异常
@SuppressWarnings("unchecked")
public T get(int i) throws Exception{
if(i>0&&i<this.len){
return (T)this.element[i];
}else{
throw new Exception("顺序表"+i+"位置没有数据");
}
}
/***********************************更新********************************/
//对i位置的数据进行更新
public void set(int i,T t) throws Exception{
if(i>0&&i<this.len){
this.element[i] = (Object)t;
} else if (i>this.len&&i<this.size){
throw new Exception("不存在"+i+"位置的数据");
} else{
throw new IndexOutOfBoundsException("数组越界");
}
}
/***********************************插入********************************/
//顺序插入
public void insert(T t) throws Exception{
if(t == null){
throw new NullPointerException("插入数据不允许为null");
}
if(this.len<this.size){
this.element[this.len++] = (Object)t;
} else {
throw new Exception("顺序表容量为"+this.size+"当前顺序表长度为"+this.size);
}
}
//在i位置插入元素
public void insert(int i,T t) throws Exception {
if(t == null){
throw new NullPointerException("插入数据不允许为null");
}
if(i < 0){
throw new NullPointerException("下标不允许小于0");
}
if(this.len<this.size){
//j为需要移动的索引位置
for(int j = this.len - 1; j >= i; j--){
//从尾巴开始移动
this.element[j+1] = this.element[j];
}
this.element[i] = (Object)t;
this.len++;
} else {
throw new Exception("顺序表容量为"+this.size+",当前顺序表长度为"+this.size+",已不允许再插入");
}
}
/***********************************删除********************************/
//删除指定索引的元素,若成功则返回删除的元素
public T remove(int i) throws Exception{
if(i>0&&i<this.len){
//j为要开始移动的索引位置
for(int j = i + 1;j<=this.len-1;j++){
//将后面元素的引用赋值给前面的元素
this.element[j-1] = this.element[j];
}
//最后一个元素要清空
this.element[this.len - 1] = null;
this.len--;
} else if(i>this.len&&i<this.size){
throw new Exception("不存在"+i+"位置的数据,请传入正确参数");
} else{
throw new IndexOutOfBoundsException("数组越界");
}
return null;
}
/***********************************测试********************************/
public static void main(String[] args) throws Exception{
SeqList<Integer> intList = new SeqList<Integer>(10);
intList.insert(1);
intList.insert(2);
intList.insert(3);
intList.insert(4);
intList.insert(5);
intList.remove(2);
System.out.println(Arrays.toString(intList.element));
SeqList<String> strList = new SeqList<String>(10);
strList.insert("ab");
strList.insert("ab");
strList.insert("ab");
strList.insert("ab");
strList.insert(2,"2ab");
System.out.println(Arrays.toString(strList.element));
}
小结:
这里的基本思路是采用Java的数组为底层数据结构。分别实现一个顺序表的初始化、查找、插入、修改,删除元素等操作。相对来说比较简单。唯一需要考虑的问题是插入和删除指定元素时的数据移动问题,所以我们这里就简单的分析一下:
在指定位置插入一个元素的思路:
//在i位置插入元素
public void insert(int i,T t) throws Exception {
if(t == null){
throw new NullPointerException("插入数据不允许为null");
}
if(i < 0){
throw new NullPointerException("下标不允许小于0");
}
if(this.len<this.size){
//j为需要移动的索引位置
for(int j = this.len - 1; j >= i; j--){
//从尾巴开始移动
this.element[j+1] = this.element[j];
}
this.element[i] = (Object)t;
this.len++;
} else {
throw new Exception("顺序表容量为"+this.size+",当前顺序表长度为"+this.size+",已不允许再插入");
}
}
首先,我们要在顺序表的某个索引位置插入一个元素。比如顺序表中已经有5个数据,数据序列为{1,2,3,4,5}
。我现在要在索引位置为2
的地方插入数据6。那我们要考虑什么问题呢?
- 首先我们得知这是一个长度为5的顺序表
len = 5
- 然后要在索引位置为2的地方插入一个数据。
i = 2
- 要移动多少个元素?既要移动元素的次数,插入点及后面的元素都要向后移动,所以
len - i = 5 - 2 = 3
(顺序表长度-索引位置),有三个要移动的元素,也就是要移动三次,即要循环3次。 - 那个元素先移动才能避免将需要的数据覆盖掉?插入的话,至少是后面的先移动,所以数据5先移动,数据3最后向后移动。
j = len - 1 = 5 - 1 = 4
(顺序表长度 - 1),既索引位置为4的先移动,也即是最后一个数据5先移动。
在指定位置删除一个元素的思路:
//删除指定索引的元素,若成功则返回删除的元素
public T remove(int i) throws Exception{
if(i>0&&i<this.len){
//j为要开始移动的索引位置
for(int j = i + 1;j<=this.len-1;j++){
//将后面元素的引用赋值给前面的元素
this.element[j-1] = this.element[j];
}
//最后一个元素要清空
this.element[this.len - 1] = null;
this.len--;
} else if(i>this.len&&i<this.size){
throw new Exception("不存在"+i+"位置的数据,请传入正确参数");
} else{
throw new IndexOutOfBoundsException("数组越界");
}
return null;
}
删除跟插入差不多,同现有一个顺序表中已经有5个数据,数据序列为{1,2,3,4,5}
。我现在要删除索引位置为2
的数据。那我们要考虑什么问题呢?
首先我们得知这是一个长度为5的顺序表
len = 5
要删除的数据的索引位置。
i = 2
- 要移动多少个元素?既要移动元素的次数,删除点后面的元素都要向前移动,所以
len - i = 5 - 2 = 3
(顺序表长度-索引位置),有三个要移动的元素,也就是要移动三次,即循环3次。 - 那个元素先移动才能避免将需要的数据覆盖掉?因为是删除操作,要删除的数据可以直接被覆盖掉,所以必须是删除点的直接后继元素先移动,所以数据4先移动,数据5最后向后移动。
j = i + 1 = 2 + 1 = 3
(删除索引位置 + 1)等于删除元素的直接后继元素的索引位置,既索引为3的元素先向前移动。
链表的概念
什么是链表?
链表是什么?
链表就是相对于顺序表而言,采用非物理连续存储而是逻辑上的线性关系的线性表既为“链表”,既采用了链式存储的线性表。
当每一个数据元素都和它下一个数据元素用指针链接在一起时,就形成了一个链,这个链子的头就位于第一个数据元素,这样的存储方式就是链式存储。
链表的特点
由于链表示在物理内存中是分散存储,所以为了能体现出数据元素之间的逻辑关系,每个数据在存储的同时,要配备一个指针,用于指向它的直接后继元素。既每一个数据元素都指向下一个数据元素(最后一个指向Null)
链表中数据元素的构成
链表中的每个数据元素都由两部分组成:
- 本身的信息,称为“数据域”
- 指向直接后继的指针,称为“指针域”
由数据域和指针域两部分信息组成数据元素的存储结构,称为链表的“结点”。如图上的每个结点中只包含一个指针的链表称为线性链表或单链表
头结点、头指针和首元结点
头结点:
有时候,在链表的第一个结点之前会额外增设一个结点,这个结点的数据域一般不存放信息,但有时候也会存放链表的长度等信息,此结点成为头结点
首元节点:
链表的第一个元素所在的节点,成为“首元结点”,既头节点的直接后继结点
头指针:
永远指向链表的第一个结点的位置,如果链表有头结点,头指针指向头结点;否则,头指针指向首元节点。
头结点,首元结点,头指针之间的关系如上图
链表的代码实现
链表的代码实现主要分成两部分,第一部分是结点的构成,第二部分是单链表的构成。
链表结点的代码构成:
//链表中的结点
public class Node<T> {
public T data; //数据域
public Node<T> next; //指针域
public Node(T data, Node<T> next) {//有参构造函数
this.data = data;
this.next = next;
}
public Node() {//默认构造函数
this(null,null);
}
}
由上我们可以发现,结点主要由数据域和指针域构成。
链表的功能实现:
/**
* 带头结点的单链表
*/
public class SinglyLinkedList<T> {
public Node<T> head;// 头指针,指向单链表的头结点
/********************************** 初始化 ********************************/
/**
* 默认构造方法构造空单链表。既空表
* 数据域和指针域均为null,仅有头结点
*/
public SinglyLinkedList() {
this.head = new Node<T>();
}
/**
* 以数据域数组构造单链表(头尾插入)
* @param type
* @param element
*/
public SinglyLinkedList(int type,T[] element){
this(); //构造空表
if(type == 0){ //头插入
Node<T> node = new Node<T>(element[element.length - 1],null); //构建数组最后一个元素的结点
for(int i = element.length - 2; i >= 0; i--){ // i是索引,从后往前遍历
node = new Node<T>(element[i],node); //依次从后开始构建,每构建一个结点,其指针域就指向先前构建好的后继结点
}
this.head.next = node; //最后将头结点的指针域指向元首结点
}
if(type == 1){ //尾插入
Node<T> node = this.head; //获得头结点
for(T elem:element){ //从头遍历数据域
node.next = new Node<T>(elem,null); //一个结点一个结点的构造串联
node = node.next;
}
}
}
/**
* 单链表的深拷贝式的构造函数
* @param linkedList
*/
public SinglyLinkedList(SinglyLinkedList<T> linkedList) {
this(); //首先构造空表
Node<T> temp = linkedList.head.next; //获得源链表的首元结点
Node<T> node = this.head; //获得新链表的头结点
while(temp != null){//只要源链表的当前结点不为null,则继续一个结点一个结点的深拷贝
node.next = new Node<T>(temp.data,null); //根据源链表的首元结点数据域,构造新链表的首元结点
temp = temp.next; //到达下一个结点
node = node.next; //到达下一个结点
}
}
/*********************************** 查找 ********************************/
/**
* 判断链表是否为空表
* @return
*/
public boolean isEmpty() {
return this.head.next == null; // 如果头结点的指针域执行Null,则为空表
}
/**
* 返回单链表的长度
* @return
*/
public int length() {
int len = 0;
Node<T> node = this.head.next; // node为首元节点
while (node != null) {
len++;
node = node.next;// 如果结点不为null,那么node指向其直接后继结点
}
return len;
}
/**
* 根据插入顺序存储,返回第i个元素
* @param i
* @return
* @throws Exception
*/
public T get(int i) throws Exception {
checkException(i);
Node<T> node = this.head.next;
int count = 0; // 首元结点的位置0,头结点记作-1
while (count != i) { //获得第i个结点
count++;
node = node.next;
}
return node.data;
}
/**
* 返回首先输出的元素值在链表中的位置,如果无则返回-1
* @param t
* @return
* @throws Exception
*/
public int search(T t) throws Exception {
checkException(t);
Node<T> node = this.head.next;
int count = 0; // 首元结点的位置0,头结点记作-1
while (!node.data.equals(t)) { // 当数据域不相等,继续循环比较
node = node.next;
count++;
if (node.next == null) { // 如果已经到了末尾,还不存在,则返回-1
return -1;
}
}
return count; // 返回结点位置,首元结点开始计算,起始为0
}
/*********************************** 更新 ********************************/
/**
* 对结点i的数据域进行更新
* @param i
* @param t
* @throws Exception
*/
public void set(int i, T t) throws Exception {
checkException(i, t);
Node<T> node = this.head.next;
int count = 0; // 首元结点在链表的位置,头结点记作-1
while (count != i) { // 获得需要更新数据域的结点
count++;
node = node.next;
}
node.data = t;
}
/*********************************** 插入 ********************************/
/**
* 在结点i前插入一个新的元素
* @param i
* @param t
* @throws Exception
*/
public void insert(int i, T t) throws Exception {
checkException(i, t);
if (i == 0) { // 在首元结点处插入新元素
this.head.next = new Node<T>(t, this.head.next);
} else {
Node<T> node = this.head.next;
int count = 0; // 首元结点的位置,既0,这里把头结点的位置记做-1。非长度
while (count != i - 1) { // 获得插入点的直接前驱结点
count++;
node = node.next;
}
node.next = new Node<T>(t, node.next);
}
}
/**
* 在链尾插入一个新元素
* @param t
* @throws Exception
*/
public void insert(T t) throws Exception {
if (this.head.next == null) {
this.head.next = new Node<T>(t, null);
} else {
Node<T> node = this.head.next; // 获得首元结点
while (node.next != null) { // 获得链表最后一个结点,该结点指针域指向null
node = node.next;
}
node.next = new Node<T>(t, null); // 最后结点的指针域执行一个数据域为t的结点
}
}
/*********************************** 删除 ********************************/
/**
* 删除指定结点i
* @param i
* @return
* @throws Exception
*/
public T remove(int i) throws Exception {
checkException(i);
if (i == 0) { // 当删除的是首元结点时
Node<T> temp = this.head.next;
this.head.next = this.head.next.next; // 删除点的前驱结点的指针域指向删除点的后驱结点
return temp.data;
} else { // 删除的是除首元结点以外的结点
Node<T> node = this.head.next;
int count = 0;
while (count != i - 1) { // 记住删除点的直接前驱结点
node = node.next;
count++;
}
Node<T> temp = node.next; // 存储删除点的数据,待返回
node.next = node.next.next; // 删除点的前驱结点的指针域指向删除点的后驱结点
return temp.data;
}
}
/*********************************** 工具 ********************************/
/**
* 单链表的比较
*/
@SuppressWarnings("unchecked")
public boolean equals(Object o) {
if (o == this) {// 如果指向的是同一个地址,返回true
return true;
}
if (!(o instanceof SinglyLinkedList)) {// 如果类型不是SinglyLinkedList,返回false
return false;
}
Node<T> node = this.head.next;
Node<T> temp = ((SinglyLinkedList<T>) o).head.next;
while (node != null && temp != null && node.data.equals(temp.data)) {
node = node.next; // node和temp同时不为null,且对应结点的data也相等,就指向下一个结点
temp = temp.next;
}
return node == null && temp == null; // 只有node和temp同时为null,才说明链表长度相等
}
/**
* 返回单链表所有元素的描述字符串
*/
public String toString() {
String str = "(";
Node<T> node = this.head.next;
while (node != null) {
str += node.data.toString();
if (node.next != null) {
str += " , ";// 不是最后一个节点时厚加分隔符
}
node = node.next;
}
return str + ")";// 空表返回
}
/**
* 异常检查
* @param i 结点位置
* @param t 数据域
* @throws Exception
*/
public void checkException(int i, T t) throws Exception {
if (isEmpty()) { // 链表没有数据
throw new Exception("这是一个空表");
}
if (i < 0 || i >= length() || i > Integer.MAX_VALUE) {
throw new Exception("结点位置越界越界"); // 传入的结点位置i不允许越界
}
if (t == null) {
throw new NullPointerException("元素不能为null"); // 传入元素不能为null
}
}
/**
* 异常检查
* @param t 数据域
* @throws Exception
*/
public void checkException(T t) throws Exception {
if (isEmpty()) {
throw new Exception("这是一个空表");
}
if (t == null) {
throw new NullPointerException("元素不能为null");
}
}
/**
* 异常检查
* @param i 结点位置
* @throws Exception
*/
public void checkException(int i) throws Exception {
if (isEmpty()) {
throw new Exception("这是一个空表");
}
if (i < 0 || i >= length() || i > Integer.MAX_VALUE) {
throw new Exception("结点位置越界越界");
}
}
/**
* test
* @param args
* @throws Exception
*/
public static void main(String[] args) throws Exception {
Integer[] integer = {1,2,3,4};
SinglyLinkedList<Integer> linkedList = new SinglyLinkedList<Integer>();
linkedList.insert(1);
linkedList.insert(2);
linkedList.insert(3);
linkedList.insert(4);
linkedList.insert(1, 9);
linkedList.set(2, 10);
linkedList.remove(4);
SinglyLinkedList<Integer> linkedList2 = new SinglyLinkedList<Integer>();
linkedList2.insert(1);
linkedList2.insert(2);
linkedList2.insert(3);
linkedList2.insert(4);
System.out.println(linkedList.length());
System.out.println(linkedList.get(2));
System.out.println(linkedList.toString());
System.out.println(linkedList.search(10));
SinglyLinkedList<Integer> linkedList3 = new SinglyLinkedList<Integer>(1,integer);
System.out.println(linkedList3.toString());
}
}
以上是代码的一些功能实现:
- 默认构建一个空链表
- 通过元素序列进行构建链表(By 头插入和尾插入两种方式)
- 单链表的深拷贝式的构造
- 判断链表是否为空表
- 返回链表的长度
- 返回第i个结点的数据域
- 查找链表中首个关键字的位置
- 对某个结点进行数据域的更新
- 在某个结点位置插入一个新结点
- 在链尾插入一个结点
- 删除指定结点
- 单链表的比较函数
这里的单链表采用有头结点的方式。但在相关处理中,为了更好的显示一些特征的区别,所以我还是把首元结点和其他结点的插入、删除操作做了代码上的区别对待。
顺序表和单链表的优缺点比较
顺序表的优缺点:
- 顺序表使用物理连续的内存空间来存放数据元素,具有很大的逻辑性,便于理解。
- 顺序表优点是其随机存取非常快速,比如要做定点修改和查找操作时(set/select)效率高,直接通过索引既可访问到。缺点是因为逻辑上相邻的元素物理上也相邻,所以插入删除需要移动插入(删除)点及其后面的所有元素。
单链表的优缺点:
- 链式存储的数据元素在物理结构没有限制,当内存空间中没有足够大的连续的内存空间供顺序表使用时,可能使用链表能解决问题。可以利用上一些内存碎片)
- 链表的优点是插入删除操作非常高效,只需要记住改变位置的前后项,通过改变指针的指向即可,不需要移动插入或删除位置的后续元素,缺点随机访问效率低,必须一个结点一个结点的遍历过去,不像顺序表直接通过索引位置即可访问,
参考资料
首先强烈推荐学习数据结构的朋友,直接去看该网站学习,作者是@严长生。里面的资料是基于严蔚敏的《数据结构(C语言版)》,本文的概念知识都是基于该网站和《数据结构(C语言版)》这个书来分析的。
数据结构概述 - 作者:@严长生
代码实现部分,主要根据自己的实现再参考大佬,发现不足,加以修改的。(原作更好!!)
Github - doubleview/data-structure