数据结构和算法--线性表

176 阅读8分钟

线性表

定义

  • 线性表是最基本、最简单、也是最常用的一种数据结构。一个线性表是n个具有相同特性的数据元素的有限序列。
  • 前驱元素: 若A元素在B元素的前面,则称A为B的前驱元素
  • 后继元素: 若B元素在A元素的后面,则称B为A的后继元素

特征 数据元素之间具有—种“—对—”的逻辑关系。

  1. 第一个数据元素没有前驱,这个数据元素被称为头结点;
  2. 最后一个数据元素没有后继,这个数据元素被称为尾结点;
  3. 除了第一个和最后一个数据元素外,其他数据元素有且仅有一个前驱和一个后继。

分类∶

  • 线性表中数据存储的方式可以是顺序存储,也可以是链式存储
  • 按照数据的存储方式不同,可以把线性表分为顺序表链表

顺序表

定义

顺序表是在计算机内存中以数组的形式保存的线性表,线性表的顺序存储是指用一组地址连续的存储单元,依次存储线性表中的各个元素、使得线性表中再逻辑结构上响铃的数据元素存储在相邻的物理存储单元中,即通过数据元素物理存储的相邻关系来反映数据元素之间逻辑上的相邻关系。

简单设计实现步骤

  1. 定义一个数组,用来存储数据
  2. 定义一个int类型数,来记录线性表的长度
  3. 通过构造方法初始化线性表
  4. 设计一些线性表的方法
    • private void clear():清空线性表
    • private boolean isEmpty():判断线性表是否为空
    • private int length():获取线性表中的元素个数
    • private T get(int i):读取并返回线性表中的第i个元素的值
    • private void insert(T t, int i):在线性表中第i个元素的位置插入一个值
    • private void insert(T t):在线性表最后添加元素
    • private T remove(int i):删除并返回线性表的第i个元素
    • private int indexOf(T t):返回线性表中首次出现指定元素的下标索引
    • private void resize(int newSize):线性表的扩容和缩容
package com.study.linear;

public class SequenceList<T> {

    // 存储元素的数组
    private T[] eles;
    // 当前线性表的长度
    private int N;

    // 构造函数进行初始化
    public SequenceList(int capacity) {
        this.eles = (T[]) new Object[capacity];
        this.N = 0;// 初始线性表没有值,长度为0
    }

    // 清空线性表
    private void clear() {
        this.N = 0;
    }

    // 判断线性表是否为空
    private boolean isEmpty() {
        return this.N == 0;
    }

    // 获取线性表中的元素个数
    private int length() {
        return this.N;
    }

    // 读取并返回线性表中的第i个元素的值
    private T get(int i) {
        T current = eles[i];
        return current;
    }

    // 在线性表中第i个元素的位置插入一个值
    private void insert(T t, int i) {
        // 先判断事先定义的数组容量够不够,不够就扩容,大小为原数组的两倍
        if (N==eles.length){
            resize(2*eles.length);
        }
        N++;//插入,线性表的长度增加
        // 先把i后面的元素都往后移
        for (int j = N - 1; j > i; j--) {
            eles[j] = eles[j - 1];
        }
        // 再把新值插入
        eles[i] = t;
    }

    // 在线性表最后添加元素
    private void insert(T t) {
        // 先判断事先定义的数组容量够不够,不够就扩容,大小为原数组的两倍
        if (N==eles.length){
            resize(2*eles.length);
        }
        eles[N++] = t;
    }

    // 删除并返回线性表的第i个元素
    private T remove(int i) {
        // 记录i位置删除的元素
        T remo = eles[i];
        // i位置后面的元素往前移
        for (int j = i; j < N-1; j++) {
            eles[j] = eles[j + 1];
        }
        N--;//删除,线性表的元素减少
        if (N<eles.length/4){//大于1/4,就减少一半
            resize(eles.length/2);
        }
        return remo;
    }

    // 返回线性表中首次出现指定元素的下标索引
    private int indexOf(T t) {
        for (int i = 0; i < N; i++) {
            if (eles[i].equals(t)){
                return i;
            }
        }
        return -1;// 没找到
    }

    // 线性表的扩容和缩容
    // 发生在 添加元素和删除元素的时候
    // 添加元素:扩容,因为我们之前定义的线性表大小是固定的
    // 删除元素:缩容,减少空间浪费
    private void resize(int newSize){// 根据newSize重置线性表的大小
        //定义临时数组,指向原数组
        T[] temp = eles;
        // 创建新数组
        eles = (T[]) new Object[newSize];
        // 将临时数组的值传入到新数组中
        for (int i = 0; i < N; i++) {
            eles[i]=temp[i];
        }
    }


    public static void main(String[] args) {
        SequenceList<String> sl = new SequenceList<>(5);
        // 插入值
        sl.insert("小红");
        sl.insert("小蓝");
        sl.insert("小白");
        sl.insert("小天",1);
        System.out.println("线性表中的数据为:");

        for (int i = 0; i < sl.length(); i++) {
            System.out.println(sl.get(i));
        }

        System.out.println("当前线性表的长度为:"+sl.length());
        // 删除值
        String remove = sl.remove(1);
        System.out.println("线性表删除的元素为:"+remove);
        System.out.println("当前线性表的长度为:"+sl.length());
        // 找特定值
        int index = sl.indexOf("小白");
        if (index!=-1){
            System.out.println("小白所在线性表的下标索引为:"+index);
        }else {
            System.out.println("线性表中找不到小白");
        }
        // 清空表
        sl.clear();
        // 判断表是否被情空
        if (sl.isEmpty()){
            System.out.println("线性表已经被清空");
        }else {
            System.out.println("清空线性表失败");
        }
    }
}

链表

定义

链表是一种物理存储单元上非连续、非顺序的存储结构,其物理结构不能只管的表示数据元素的逻辑顺序,数据元素的逻辑顺序是通过链表中的指针链接次序实现的。链表由一系列的结点(链表中的每一个元素称为结点)组成,结点可以在运行时动态生成。

分类

  • 单向链表
  • 双向链表

单向链表

定义

单向链表是链表的一种,它由多个结点组成,每个结点都由一个数据域和一个指针域组成,数据域用来存储数据,指针域用来指向其后继结点。链表的头结点的数据域不存储数据,指针域指向第一个真正存储数据的结点。

简单设计实现步骤

  1. 定义一个Node类,来实现链表
    • 定义一个结点,用来存储数据
    • 定义一个Node结点,来标志下一个结点
    • 无参构造器
  2. 定义一个head结点作为头结点
  3. 定义一个int类型的数记录链表的长度
  4. 设计一些链表的方法
    • private void clear():清空链表
    • private boolean isEmpty():判断链表是否为空
    • private int length():获取链表中的元素个数
    • private T get(int i):读取并返回链表中的第i个元素的值
    • private void insert(T t, int i):在链表中第i个元素的位置插入一个值
    • private void insert(T t):在链表最后添加元素
    • private T remove(int i):删除并返回链表的第i个元素
    • private int indexOf(T t):返回链表中首次出现指定元素的下标索引
    • public void reserve():整个链表反转
    • public Node reserve(Node node):链表中的某个结点反转,并返回反转后的结点
package com.study.linear;

public class LinkList<T>{

    private class Node{
        T item;// 结点存储的数据
        Node next;// 下一个结点

        public Node(T item, Node next) {
            this.item = item;
            this.next = next;
        }
    }

    private Node head;// 头结点
    private int N;// 记录链表长度

    public LinkList(){
        this.head =new Node(null,null);//初始化头结点
        this.N=0;//初始化链表元素个数
    }

    // 清空链表
    private void clear(){
        this.head=null;
        this.N = 0;
    }
    // 获取链表的长度
    private int length(){
        return N;
    }
    // 判断链表是否为空
    private boolean isEmpty(){
        return this.N==0;
    }
    // 获取指定位置i的元素
    private T get(int i){
        // 通过循环从头节点开始找,找到位置i的元素
        Node n = head.next;
        for (int j = 0; j < i; j++) {
            n = n.next;
        }
        return n.item;
    }
    // 向链表中添加元素
    private void insert(T t){
        // 通过循环,找到最后一个结点
        Node n = head;
        while(n.next!=null){
            n = n.next;
        }
        // 创建一个新结点,将要添加的元素放进新结点
        Node newNode = new Node(t, null);
        // 最后一个结点,指向新结点
        n.next = newNode;
        // 长度加一
        N++;
    }
    // 在指定位置i添加元素
    private void insert(T t,int i){
        // 找到位置i的前一个结点
        Node n = head;
        for (int j = 0; j < i; j++) {
            n = n.next;
        }
        // 创建一个新结点,将要添加的元素放进新结点,新结点指向位置i的结点
        Node newNode = new Node(t, n.next);
        // 前一个结点指向新结点
        n.next=newNode;
        // 元素个数加一
        N++;
    }
    // 删除指定位置i元素
    private T remove(int i){
        // 找到位置i的前一个结点
        Node n = head;
        for (int j = 0; j < i; j++) {
            n = n.next;
        }//循环结束,n就是位置i的前一个结点
        // 找到位置i的后一个结点
        Node currentNode = n.next;//位置i的结点
        Node NextNode = currentNode.next;//位置i的下一个结点
        // 位置i的前一个结点指向位置i的后一个结点
        n.next=NextNode;
        // 元素个数减少
        N--;
        return currentNode.item;
    }

    // 获取元素在链表中出现的第一次的位置
    private int indexOf(T t){
        Node n = head;
        for (int i = 0; n.next!=null; i++) {
            n = n.next;
            if (n.item.equals(t)){
                return i;
            }
        }
        return -1;
    }

    // 整个链表反转
    public void reserve(){
        if (isEmpty()){ // 如果是空链表就不反转,直接结束
            return;
        }
        reserve(head.next);//调用重载方法,进行反转
    }
    // 链表中的某个结点反转,并返回反转后的结点
    public Node reserve(Node node){
        if (node.next==null){
            head.next = node;
            return node;
        }
        // 递归反转当前结点的下一个结点, 返回值为反转后的 当前结点的前一个结点
        //  原:   head->1->2->3
        // 反转后: head->3->2->1
        // 如,当前结点为1,反转 1的下一个结点2,得到的2 是反转后结点1的前一个结点
        Node pre = reserve(node.next);
        // 当前结点指向返回的结点的下一个结点
        pre.next = node;
        // 当前结点的下一个结点变为null
        node.next=null;
        return node;
    }
    public static void main(String[] args) {
        LinkList<String> il = new LinkList<>();
        il.insert("xiao");
        il.insert("tai");
        il.insert("yang");
        il.insert("****",1);
        System.out.print("添加元素后,链表的数据为:");
        for (int i = 0; i < il.length(); i++) {
            if (i!=il.length()-1){
                System.out.print(il.get(i)+",");
            }else {
                System.out.println(il.get(i));
            }
        }
        il.reserve();
        System.out.print("链表反转后的数据为:");
        for (int i = 0; i < il.length(); i++) {
            if (i!=il.length()-1){
                System.out.print(il.get(i)+",");
            }else {
                System.out.println(il.get(i));
            }
        }
        System.out.println("当前链表的长度为:"+il.length());
        String remove = il.remove(1);
        System.out.println("链表删除的元素是:"+remove);
        System.out.println("当前链表的长度为:"+il.length());
        String s = il.get(2);
        System.out.println("位置2的数据是:"+s);

        int m = il.indexOf("xiao");
        if (m!=-1){
            System.out.println("xiao第一次在链表出现的位置是:"+m);
        }else {
            System.out.println("xiao在链表中没有出现过~");
        }
    }
}

双向链表

定义

双向链表也叫双向表,是链表的一种,它由多个结点组成每个结点都由一个数据域和两个指针域组成,数据域用来存储数据,其中一个指针域用来指向其后继结点另一个指针域用来指向前驱结点。链表的头结点的数据域不存储数据指向前驱结点的指针域值为null指向后继结点的指针域指向第一个真正存储数据的结点。

简单设计实现步骤

  1. 创建一个结点类Node来实现链表
    • 创建数据域用来存储数据
    • 创建前驱结点pre
    • 创建后继结点last
    • 创建有参构造
  2. 创建head头结点
  3. 创建last尾结点
  4. 创建int类型整数N来记录链表的长度
  5. 设计一些链表的方法
    • private void clear():清空链表
    • private boolean isEmpty():判断链表是否为空
    • private int length():获取链表中的元素个数
    • private T get(int i):读取并返回链表中的第i个元素的值
    • private void insert(T t, int i):在链表中第i个元素的位置插入一个值
    • private void insert(T t):在链表最后添加元素
    • private T remove(int i):删除并返回链表的第i个元素
    • private int indexOf(T t):返回链表中首次出现指定元素的下标索引
    • public T getFirst():获取第一个元素
    • public T getLast():获取最后一个元素
package com.study.linear;

public class TwoWayList<T> {
    private Node head;//头结点
    private Node last;//尾结点
    private int N;//链表长度

    // 结点类
    private class Node{
        private T item;
        private Node pre;
        private Node next;

        public Node(T item, Node pre, Node next) {
            this.item = item;
            this.pre = pre;
            this.next = next;
        }
    }
    // 初始化链表
    public TwoWayList(){
        // 初始化头结点
        this.head=new Node(null,null,null);
        // 初始化尾结点
        this.last=null;
        // 初始化元素个数
        this.N=0;
    }

    // 清空链表
    public void clear(){
        this.head.next=null;
        this.last=null;
        this.N=0;
    }

    // 判断链表是否为空
    public boolean isEmpty(){
        return this.N==0;
    }

    // 获取链表中的元素个数
    public int length(){
        return N;
    }

    // 获取并返回链表中的第i个元素的值
    public T get(int i){
        // 获取第i-1个元素的结点
        Node n = head;
        for (int j = 0; j < i; j++) {
            n = n.next;
        }
        return n.next.item;
    }

    // 往链表中添加值
    public void insert(T t){
        Node n = head;
        if (isEmpty()){// 链表为空
            // 创建新的结点
            Node newNode = new Node(t,head,null);
            // 新结点变成尾结点
            last = newNode;
            // 头结点指向尾结点
            head.next = last;
        }else { // 链表不为空
            // 创建新的结点
            Node newNode = new Node(t,last,null);
            // 当前尾结点指向新结点
            last.next = newNode;
            // 让新结点成为尾结点
            last = newNode;
        }
        // 添加完成,元素个数+1
        N++;
    }

    // 在第i个位置添加值
    public void insert(T t,int i){
        Node n = head;
        // 第i-1位置的结点
        for (int j = 0; j < i; j++) {
            n = n.next;
        }
        // 第i位置结点
        Node currentNode = n.next;
        // 创建新结点
        Node newNode = new Node(t,n,currentNode);
        // i-1结点的尾结点指向新结点
        n.next = newNode;
        // i结点的头结点指向新节点
        currentNode.pre = newNode;
        // 添加完,元素个数加一
        N++;
    }

    // 删除并返回链表中第i个位置的值
    public T remove(int i){
        Node n = head;
        for (int j = 0; j < i; j++) {
            n = n.next;
        }
        // i位置的结点
        Node currentNode = n.next;
        // i+1位置的结点
        Node nextNode = currentNode.next;
        // i-1的结点指向i+1的结点
        n.next = nextNode;
        nextNode.pre = currentNode;
        // 删除元素,元素个数减一
        N--;
        return currentNode.item;
    }

    // 返回链表中指定位置的元素的下标索引
    public int indexOf(T t){
        Node n = head.next;
        for (int i = 0; n.next!=null; i++) {
            if (n.item.equals(t)){
                return i;
            }
            n = n.next;
        }
        return -1;
    }

    // 获取第一个元素
    public T getFirst(){
        return head.next.item;
    }
    // 获取最后一个元素
    public T getLast(){
        return last.item;
    }

    public static void main(String[] args) {
        TwoWayList<String> il = new TwoWayList<>();
        il.insert("xiao");
        il.insert("tai");
        il.insert("yang");
        il.insert("****",1);
        System.out.print("添加元素后,链表的数据为:");
        for (int i = 0; i < il.length(); i++) {
            if (i!=il.length()-1){
                System.out.print(il.get(i)+",");
            }else {
                System.out.println(il.get(i));
            }
        }
        System.out.println("当前链表的长度为:"+il.length());
        System.out.println("当前链表第一个元素是:"+il.getFirst());
        System.out.println("当前链表最后一个元素是:"+il.getLast());
        String remove = il.remove(1);
        System.out.println("链表删除的元素是:"+remove);
        System.out.println("当前链表的长度为:"+il.length());
        String s = il.get(2);
        System.out.println("位置2的数据是:"+s);

        int m = il.indexOf("tai");
        if (m!=-1){
            System.out.println("tai第一次在链表出现的位置是:"+m);
        }else {
            System.out.println("tai在链表中没有出现过~");
        }
    }

}

ArrayList 和 LinkedList

ArrayList

  1. 用数组实现
  2. 有对数组进行扩容操作
  3. 提供了遍历方式

查看源码,分析add方法 image.png

image.png

image.png

image.png

image.png

LinkedList

  1. 用双向链表实现
  2. 结点类有三个指针域

查看源码,分析add方法

image.png

image.png

image.png

顺序表 vs 链表

  1. 顺序表查询数据快,增删数据慢
  2. 链表增删数据快,查询数据慢 (查询中间值的话,引入快慢指针进行查询,更快
  3. 顺序表用数组实现,物理地址是连续的
  4. 链表用指针域实现,物理地址是随机的,不规则的
  5. 顺序表一次性开辟固定大小的数组,除了动态修改数组的大小,不然就可能产生空间浪费或者数组不够用,部分数据没法存储
  6. 链表是动态开辟空间,需要的时候在开辟,不会产生空间浪费

其他知识点的博客参考

  1. 链表的快慢指针
  • 链表的快慢指针以及判断链表是否有环
  • 有环的话,求环的入口和长度
  1. 循环链表和约瑟夫问题