数组是存放在连续内存空间上的相同类型数据的集合。可以方便的通过下标索引的方式获取到下标下对应的数据。
不能单独删除、释放数组中的某个元素,只能覆盖。如果要释放,就是全释放(程序运行结束,回收内存栈空间)。
因为数组的在内存空间的地址是连续的,所以我们在删除或者增添元素的时候,就难免要移动其他元素的地址。对于数组来说,在尾部插入、删除元素是比较高效的,时间复杂度是 O(1),但是如果在中间或者开头插入、删除元素,就会涉及数据的搬移,时间复杂度为 O(N),效率较低。
技巧:
把待删除元素交换到最后一个,然后再删除,就可以避免数据搬移。
使用双指针法,可原地修改数组
链表是一种通过指针串联在一起的线性结构,每一个节点由两部分组成,一个是数据域一个是指针域(存放指向下一个节点的指针),最后一个节点的指针域指向null(空指针的意思)。
链接的入口节点称为链表的头结点也就是head。
每一个节点有两个指针域,一个指向下一个节点,一个指向上一个节点。
双链表既可以向前查询也可以向后查询。
块状链表结合了数组和链表的特性,将连续成段的数组通过链接串起来。块状链表的特点是插入很灵活,寻找特定元素也比正常链表快速。
链表在内存中可不是连续分布的。
链表是通过指针域的指针链接在内存中各个节点。所以链表中的节点在内存中不是连续分布的 ,而是散乱分布在内存中的某地址上,分配机制取决于操作系统的内存管理。
这个链表起始节点为2, 终止节点为7, 各个节点分布在内存个不同地址空间上,通过指针串联在一起。
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;
}
}
只要将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)。
在一个链表中插入新元素分为以下三种情况:
将新节点的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++;
}
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;
}
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;
}
链表的一大问题就是操作当前节点必须要找前一个节点才能操作。这就造成了,头结点的尴尬,因为头结点没有前一个节点了。
链表中每次对应头结点的情况都要单独处理,所以使用虚拟头结点的技巧,就可以解决这个问题。
设置虚拟头节点dummyNode,dummyNode.next 指向head,对原头节点的操作即可和其它节点统一,更加方便
数组在定义的时候,长度就是固定的,如果想改动数组的长度,就需要重新定义一个新的数组。