数据结构——数组与链表

一、数组理论基础

数组是存放在连续内存空间上的相同类型数据的集合。可以方便的通过下标索引的方式获取到下标下对应的数据。

不能单独删除、释放数组中的某个元素,只能覆盖。如果要释放,就是全释放(程序运行结束,回收内存栈空间)。

因为数组的在内存空间的地址是连续的,所以我们在删除或者增添元素的时候,就难免要移动其他元素的地址。对于数组来说,在尾部插入、删除元素是比较高效的,时间复杂度是 O(1),但是如果在中间或者开头插入、删除元素,就会涉及数据的搬移,时间复杂度为 O(N),效率较低。

技巧:
把待删除元素交换到最后一个,然后再删除,就可以避免数据搬移。

使用双指针法,可原地修改数组

二、链表理论基础

链表是一种通过指针串联在一起的线性结构,每一个节点由两部分组成,一个是数据域一个是指针域(存放指向下一个节点的指针),最后一个节点的指针域指向null(空指针的意思)。
链接的入口节点称为链表的头结点也就是head。

1.链表分类

1)单链表

单链表中的节点只能指向下一个节点。
在这里插入图片描述

2)双链表

每一个节点有两个指针域,一个指向下一个节点,一个指向上一个节点。
双链表既可以向前查询也可以向后查询。
在这里插入图片描述

3)循环链表

链表首尾相连
在这里插入图片描述

4)块状链表

块状链表结合了数组和链表的特性,将连续成段的数组通过链接串起来。块状链表的特点是插入很灵活,寻找特定元素也比正常链表快速。

2.链表的存储方式

链表在内存中可不是连续分布的。
链表是通过指针域的指针链接在内存中各个节点。所以链表中的节点在内存中不是连续分布的 ,而是散乱分布在内存中的某地址上,分配机制取决于操作系统的内存管理。
在这里插入图片描述
这个链表起始节点为2, 终止节点为7, 各个节点分布在内存个不同地址空间上,通过指针串联在一起。

3.链表的定义

C/C++的定义链表节点方式:

// 单链表
struct ListNode {
    int val;  // 节点上存储的元素
    ListNode *next;  // 指向下一个节点的指针
    ListNode(int x) : val(x), next(NULL) {}  // 节点的构造函数
};

Java定义链表节点:

public class ListNode {
    // 结点的值
    int val;
    // 下一个结点
    ListNode next;
    // 节点的构造函数(无参)
    public ListNode() {
    }
    // 节点的构造函数(有一个参数)
    public ListNode(int val) {
        this.val = val;
    }
    // 节点的构造函数(有两个参数)
    public ListNode(int val, ListNode next) {
        this.val = val;
        this.next = next;
    }
}

Java单向链表定义:

public class LinkedList {
    static class ListNode(
        int val; 
        ListNode next;
        public ListNode(int val) {
            this.val = val;
    }
    ListNode head; //头节点
    ListNode tail;//尾节点
    int size;
    public LinkedList() { //初始化
        head = null;
        tail = null;
        size = 0;
    }
}

4.链表的操作

1)删除节点

在这里插入图片描述
只要将C节点的next指针 指向E节点就可以了。
C/C++里要手动释放D节点内存,有自动回收机制(Java、Python)则不用

public void delete(int number) {
    if(head != null && head.val == number) { //删除头节点
        head = head.next;
        size--;
        if(size == 0) { //没有剩余元素
            tail = head;
        }
    } else {//删除非头节点
        ListNode prev = head;
        ListNode cur = head;
        while (prev != null && cur != null) {
            if (cur.val == number) {
                if(cur == tail) { //删除末尾元素
                    tail = prev;
                }
                prev.next = cur.next;
                size--;
                return;
            }
            prev = cur;
            cur = cur.next;
        }
    }
}

链表的增添和删除都是 O ( 1 ) O(1) O(1)操作,也不会影响到其他节点。

但是要注意,要是删除第五个节点,需要从头节点查找到第四个节点通过next指针进行删除操作,查找的时间复杂度是 O ( n ) O(n) O(n)

2)插入节点

在一个链表中插入新元素分为以下三种情况:

  • 插入到链表的最前头,作为新的头结点。
  • 插入到链表中间的位置。
  • 插入到链表的尾部,作为链表中最后的元素。

将新节点的next指针指向插入位置后的节点,再将插入节点前的next指针指向新插入的节点。
在这里插入图片描述
注意:
我们必须先执行步骤1,再执行步骤2;如果先执行步骤2,否则会导致插入位置后续的节点无法被找到。

public void insert(int position, int number) {
    if (position > size) {
        return;
    }
    ListNode newNode = new ListNode(number);
    if (position == 0) {
        newNode.next = head;
        head = newNode;
        if(tail == null) {
            tail = newNode;
        }
        size++;
    } else if (position == size) {
        this.append(number);
    } else {
        ListNode prev = head;
        for (int i = 0; i < position - 1; i++) {
            prev = prev.next;
        }
        ListNode next = prev.next;
        newNode.next = next;
        prev.next = newNode;
        size++;
    }
}
//末尾增添新元素
public void append(int number) { 
    ListNode newNode = new ListNode(number);
    if(tail == null) {
        tail = newNode;
    } else {
        tail.next = newNode;
        tail = newNode;
    }
    size++;
}

3)查找元素

public int search(int number) {
    ListNode cur = head;
    for(int index = 0; cur != null; index++) {
        if(cur.val == number) {
            return index;
        }
        cur = cur.next;
    }
    return -1;
}

4)更新元素

public int update(int oldValue, int newValue) {
    ListNode cur = head;
    for(int index = 0; cur != null; index++) {
        if(cur.val == oldValue) {
            cur.val = newValue;
            return index;
        }
        cur = cur.next;
    }
    return -1;
}

5.虚拟头节点的使用

链表的一大问题就是操作当前节点必须要找前一个节点才能操作。这就造成了,头结点的尴尬,因为头结点没有前一个节点了。

链表中每次对应头结点的情况都要单独处理,所以使用虚拟头结点的技巧,就可以解决这个问题。

设置虚拟头节点dummyNode,dummyNode.next 指向head,对原头节点的操作即可和其它节点统一,更加方便

6.与数组的对比

数组在定义的时候,长度就是固定的,如果想改动数组的长度,就需要重新定义一个新的数组。

链表的长度可以是不固定的,并且可以动态增删, 适合数据量不固定,频繁增删,较少查询的场景。
在这里插入图片描述

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值