数据结构与算法——基础数据结构(数组和链表)

什么是线性表

线性表是由零个或多个数据元素组成的有限序列。

  1. 线性表是有限的
  2. 线性表第一个元素无前驱,最后一个元素无后继,中间元素只有一个前驱和后继元素
  3. 线性表可以是空表

常见的线性表

常见的线性表如下图所示

常见的线性表数据结构有:数组,链表,队列,栈;与线性表相对的是稍稍复杂的数据结构,比如:树,图等。

什么是数组

数组是一种线性表,具有连续的内存空间,并且只能存储相同的数据类型。

数组有什么特性

  1. 数组支持随机访问,根据下标具有高效的查询性能

       计算机会为内存单元分配内存地址,当要访问数组中第n个元素时,计算机根据寻址公式,可以快速定位第n个元素的内存地址,从而实现数组中元素的随机访问。

       a[n]_address = a[0]_address + data_type_size*n

       试想,我们怎样定义一个数组,int[]  array = new int[10];

       这里会得到一个变量名,这个变量名其实就是指向数组第一个元素地址的指针!当我们利用数组的引用变量去获取数组中的某个元素时,例如array[1]​​​​​​​,这时就会根据寻址公式得到array[1]的地址(假设首地址是1001):

      array[1].address = 1001 + 4*1 = 1005

    2. 数组随机插入数据,随机删除数据性能较差

       得益于连续的内存空间,数组随机访问某个元素的时间复杂度是O(1),也因为要保持数组的这个特性(连续内存空间存储数据),数组的随机插入数据和随机删除数据都必须在操作完成后,做大量的数据搬移。

      这里实现了一个数组的增删改查案例:

package algorithm;

import java.util.Arrays;

/**
 * 数组增删改查测试
 *
 * @author zab
 * @date 2019/5/23 8:54
 */
public class ArrayTest {
    /**
     * 数组初始容量大小
     */
    private Integer init;
    /**
     * 数组定义
     */
    private Integer[] array;
    /**
     * 数组当前下标,表示最后一个存值的元素下一个位置
     */
    private int currentIndex;

    public ArrayTest() {
        init = 10;
        array = new Integer[init];
    }

    public ArrayTest(int init) {
        this.init = init;
        array = new Integer[init];
    }

    /**
     * 增,在数组末尾
     */
    public Integer[] add(Integer i) {
        if (currentIndex >= array.length * 0.8) {
            //扩容
            this.expansion();
        }
        array[currentIndex++] = i;
        return array;
    }

    /**
     * 增,指定位置
     */
    public Integer[] add(Integer index, Integer value) {
        //边界判断
        if (index < 0 || index > array.length - 1) {
            return null;
        }
        if (currentIndex >= array.length * 0.8) {
            //扩容
            this.expansion();
        }
        for (int i = currentIndex; i > index; i--) {
            array[i + 1] = array[i];
            if (i == index + 1) {
                array[i] = array[i - 1];
                array[index] = value;
                currentIndex++;
                break;
            }
        }
        return array;
    }

    private Integer[] expansion() {
        Integer[] arrayNew = new Integer[init << 1];
        for (int i = 0, length = array.length; i < length; i++) {
            arrayNew[i] = array[i];
        }
        this.init = arrayNew.length;
        array = arrayNew;
        //释放
        arrayNew = null;
        return array;
    }

    private Integer[] cutdown() {
        if (init == 10) {
            return null;
        }
        Integer[] arrayNew = new Integer[init >> 1];
        arrayNew = Arrays.copyOf(array, arrayNew.length);
        this.init = arrayNew.length;
        array = arrayNew;
        arrayNew = null;
        return array;
    }

    /**
     * 删,指定下标
     */
    public Integer delete(Integer index) {
        if (index < 0 || index > array.length - 1) {
            return -1;
        }
        for (int i = 0, length = currentIndex; i < length - 1; i++) {
            if (i >= index) {
                array[i] = array[i + 1];
            }
        }
        array[currentIndex-- - 1] = null;
        if (currentIndex < (array.length >> 1) * 0.8) {
            //缩容
            cutdown();
        }
        return 1;
    }

    /**
     * 设置值,指定下标
     */
    public Integer set(Integer index, Integer value) {
        if (index < 0 || index > array.length - 1) {
            return -1;
        }
        array[index] = value;
        return 1;
    }

    /**
     * 查,指定下标
     */
    public Integer get(Integer index) {
        if (index < 0 || index > array.length - 1) {
            return null;
        }
        return array[index];
    }

    @Override
    public String toString() {
        return "ArrayTest{" +
                "init=" + init +
                ", array=" + Arrays.toString(array) +
                '}';
    }
}

      可以看出,数组要实现随机删除一个元素,随机增加一个元素,需要循环做数据搬移,其时间复杂度为O(n)   。而不加先决条件笼统说数组增加删除的效率低是有问题的,数组在往最后增加删除数据的时间复杂度是O(1)。

     而对于修改操作的时间复杂度分析同样需要分不同情况

      一,如果知道数组中要修改元素的下标,修改时间复杂度为O(1),例如修改下标为1的数据为value,则直接根据下标:

array[index] = value

      二,如果仅仅知道要修改的元素值,而不知道元素在数组中的位置,则需要遍历数组,找到给定元素值的下标,再根据下标修改成想要的值,时间复杂度为O(n)。例如将数组中的元素oldValue,设置为newValue,其最简写法应该是:

public void set(Integer oldValue, Integer newValue){
    for(int i = 0,j = array.length;i < j; i++){
        if(array[i] = oldValue){
            array[i] = newValue;
            break;
        }
    }
}

什么是链表 

       链表是一种物理存储介质上非连续、非顺序的存储结构。链表由一系列结点(链表中每一个元素称为结点)组成,每个结点包括两个部分:一个是存储数据元素的数据域,另一个是存储下一个结点地址的指针域。 相比于数组,操作更复杂,由于不必须按顺序存储,链表在插入的时候可以达到O(1)的复杂度,但是查找一个节点或者访问特定编号的节点则需要O(n)的时间。

        使用链表结构可以克服数组需要预先知道数据大小的缺点,链表结构可以充分利用计算机内存空间,实现灵活的内存动态管理。但是链表失去了数组随机访问的优点,同时链表由于增加了结点的指针域,空间开销比数组大。常见链表类型有:单链表,循环链表,双向链表。

单链表

      链表将一串零散的内存块通过指针串联在一起,充分利用了零散内存,举个浅显的例子,假如内存还有150Mb内存可用,如果申请100Mb大小的数组不见得能成功,但是申请100Mb大小的链表却没有任何问题。单链表最后一个结点指向是空地址null,反过来说,如果一个链表结点的next指针指向了null,那么这个结点就是尾结点。

循环链表

     循环链表和单链表唯一的区别在于,循环链表的尾结点指向了链表的第一个结点。

双向链表     

      双向链表在单链表的基础上又多了一块内存区域保存前驱结点的地址。相比于单链表,双线链表在某些情况下的新增、删除操作更高效。这就是为什么双向链表虽然比较耗内存,但是应用却比单链表更广泛的原因。

      我们知道,单链表的新增和删除操作的时间复杂度是O(1),但是新增和删除给定指针的操作,需要知道给定指针的前驱结点的指针,而单链表根据指针获取前驱结点指针需要遍历链表,也就是说,为了新增或删除给定指针的结点,需要花O(n)的时间复杂度去查询前驱结点的指针,根据加法法则,为了完成单链表的给定指针的新增或删除操作需要总的时间复杂度为O(n)。而此种情况下的双向链表就凸显了优势,因为双向链表可以通过给定指针获取前驱结点的指针,其时间复杂度为O(1),而链表的纯新增和删除操作时间复杂度为O(1),根据加法法则,双向链表根据给定指针完成新增、删除操作的总时间复杂度为O(1)。

链表操作     

链表详细操作,见下篇。

 

  • 3
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值