【数据结构与算法】初入数据结构中的线性表(Linear table)及Java代码实现

初入数据结构中的线性表(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

猜你喜欢

转载自blog.csdn.net/snailmann/article/details/80482275
今日推荐