前言
大家好,本系列开始讲结合刷力扣的过程,对数据结构和算法进行总结。牧码心今天给大家推荐一篇数据结构与算法系列(一)—链表的文章,希望对你有所帮助。大纲如下:
- 链表概要
- 链表分类
- 链表实现
链表概要
链表是线性表的一种,相比于数组,链表是一种稍微复杂一点的数据结构,为了更好的理解链表和数组,我们先来看下这两者的区别:
- 存储结构
数组需要一块连续的内存空间来存储,对内存的要求比较高。如果我们申请一个 100MB 大小的数组,当内存中没有连续的、足够
大的存储空间时,即便内存的剩余总可用空间大于 100MB,仍然会申请失败。链表并不需要一块连续的内存空间,它通过“”指针“” 将一组零散的内存串联起来使用,所以如果我们申请的是 100MB 大小的链表,根本不会有问题。对比结构如图所示:
- 基本操作
数组和链表都支持查询,删除,插入等操作,但因为内存存储机制的不同,它们插入、删除、随机访问操作的时间复杂度也不同.数组的插入、删除操作时,为了保持内存数据的连续性,需要做大量的数据移动,所以时间复杂度 O(n),而在链表中插入或者删除一个数据,我们并不需要为了保持内存的连续性而数据移动,所以对应的时间复杂度是 O(1)。但是,链表要想随机访问第 k 个元素,就没有数组那么高效了。因为链表中的数据并非连续存储的,所以无法像数组那样,根据首地址和下标,通过寻址公式就能直接计算出对应的内存地址,而是需要根据指针一个结点一个结点地依次遍历,直到找到相应的结点,对应的时间复杂度是 O(n)。而数组可以根据下标随机访问,对应的时间复杂度是 O(1)。对比如下:
时间复杂度 | 链表 | 数组 |
---|---|---|
插入/删除 | O(1) | O(n) |
随机访问 | O(n) | O(1) |
综上分析,链表是通过指针将一组零散的内存块串联在一起,其中内存块也可以成为结点,一般结点由数据域(data)和指针域(next)构成,指针(next)用于指向其他结点的地址。
链表的类型
链表按照结构可以分为单链表,双向链表和循环链表等类型的链式结构。每种类型的链表各自有基本操作特点,下面我们分别介绍:
- 单链表
单链表由节点组成,每个节点都包含下一个节点的指针,没有环形结构。其结构示意图如下:
从图看出有两个节点比较特殊,一个是头结点,用来于记录链表的基地址,不存储数据,也是遍历整个链表的起始位置。一个是尾结点,其指针不是指向下一节点,而是指向一个空地址。下面我们用图演示,单链表的操作,如插入,删除等。 - 单链表添加节点
说明:在"节点10"与"节点20"之间添加"节点15"
- 单链表删除节点
删除"节点30",删除之后,“节点20” 的后继节点为"节点40"。
- 双向链表
双向链表也是由节点组成,它的每个数据节点都有两个指针,分别指向直接后继(next)和直接前驱(pre)。所以,从双向链表中的任意一个结点开始,都可以很方便地访问它的前驱结点和后继结点。其示意结构图如下:
从图中可以看出,双向链表需要额外的两个空间来存储后继结点和前驱结点的地址。所以,如果存储同样多的数据,双向链表要比单链表占用更多的内存空间。虽然两个指针比较浪费存储空间,但可以支持双向遍历,这样也带来了双向链表操作的灵活性。下面用图演示双向链表的操作,如如插入,删除等。 - 双链表添加节点
说明:在"节点10"与"节点20"之间添加"节点15"
- 双链表删除节点
说明:删除"节点30"
- 循环链表
循环链表是一种特殊的单链表。实际上,循环链表也很简单。它跟单链表唯一的区别就在尾结点。我们知道,单链表的尾结点指针指向空地址,表示这就是最后的结点了。而循环链表的尾结点指针是指向链表的头结点。从图中可以看出来,它像一个环一样首尾相连,所以叫作“循环”链表。其结构示意图如下:
和单链表相比,循环链表的优点是从链尾到链头比较方便。当要处理的数据具有环型结构特点时,就特别适合采用循环链表。比如著名的约瑟夫问题。
链表实现
- 单链表实现(java版)
/**
* @program:com.greekw.datastruct.linkedlist
* @desc:单链表操作demo
* 1、单链表的插入,删除,查找
* @Author:greekw
* @Date:2020-06-27 18:49
*/
public class SinglyLinkedListDemo {
// 定义一个头结点
private static Node head=null;
// 定义一个Node类封装结点数据
public static class Node{
private int data;
private Node next;
public Node(int data,Node next){
this.next=next;
this.data=data;
}
public int getData(){
return data;
}
}
public static Node createNode(int data){
return new Node(data,null);
}
// 创建链表,头插入,头插法建立链表,输入顺序是相反的,即逆序
public static void addAtHead(int data){
Node new_node=new Node(data,null);
if(head==null){
head=new_node;
}else {
new_node.next=head;
head=new_node;
}
}
// 创建链表,尾插法,在链表尾部顺序插入
public static void addAtTail(int data){
Node new_node=new Node(data,null);
// 空链表,则可以赋值给head
if(head==null){
head=new_node;
}else {
Node p=head;
//寻找尾结点,尾指针不是指向下一个结点,而是指向一个空地址 NULL
while (p.next!=null){
p=p.next;
}
new_node.next=p.next;
p.next=new_node;
}
}
// 插入在指定结点前
public static void addAtBefore(Node p,int data){
Node new_node=new Node(data,null);
if (p == null) return;
if(p==head){
addAtHead(data);
return;
}
Node q=head;
while (q!=null && q.next.data!=p.data){
q=q.next;
}
if(q==null) return ;
new_node.next=q.next;
q.next=new_node;
}
// 插入在指定结点后
public static void addAtAfter(Node p,int data){
Node new_node=new Node(data,null);
if (p == null) return;
Node q=head;
while (q!=null && q.data!=p.data){
q=q.next;
}
if(q==null) return ;
new_node.next=q.next;
q.next=new_node;
}
// 遍历链表
public static void printAt(){
Node p=head;
while (p!=null){
System.out.println(p.data);
p=p.next;
}
}
// 链表查找,根据索引查找
public static Node findByIndex(int index){
Node p=head;
int pos=0;
// 遍历链表,查找与index相同的位置
while (p!=null && pos!=index){
p=p.next;
++pos;
}
return p;
}
// 链表查找,根据值查找
public static Node findByValue(int data){
Node p=head;
while (p!=null && p.data!=data){
p=p.next;
}
return p;
}
// 链表删除节点
public static void deleteNode(Node p){
if(p==null || head==null) return;
// 删除头结点
if(head==p){
head=head.next;
return;
}
// 查找要删除的节点
Node q=head;
while (q!=null && q.next.data!=p.data){
q=q.next;
}
if(q==null) return;
q.next=q.next.next;
}
// 测试用例
public static void main(String[] args) {
for(int i=0;i<20;i++){
addAtTail(i*2);
}
//printAt();
/*Node node=findByIndex(3);
System.out.println(node.data);
Node node1=findByValue(10);
System.out.println(node1.data);*/
addAtAfter(createNode(2),7);
printAt();
addAtBefore(createNode(2),11);
printAt();
deleteNode(createNode(2));
printAt();
}
}
- 双链表实现(java版)
/**
* @program:com.greekw.datastruct.linkedlist
* @desc:双链表操作Demo
* jdk基于双链表实现的可参考 {@link LinkedList}
* @Author:greekw
* @Date:2020-07-05 17:19
*/
public class DoubleLinkedListDemo<T>{
// 表头
private DNode<T> mHead;
// 节点个数
private int mCount;
// 双向链表“节点”对应的结构体
private class DNode<T> {
public DNode prev;
public DNode next;
public T value;
public DNode(T value, DNode prev, DNode next) {
this.value = value;
this.prev = prev;
this.next = next;
}
}
// 构造函数
public DoubleLinkedListDemo() {
// 创建“表头”。注意:表头没有存储数据!
mHead = new DNode<T>(null, null, null);
mHead.prev = mHead.next = mHead;
// 初始化“节点个数”为0
mCount = 0;
}
// 返回节点数目
public int size() {
return mCount;
}
// 返回链表是否为空
public boolean isEmpty() {
return mCount==0;
}
// 获取第index位置的节点
private DNode<T> getNode(int index) {
if (index<0 || index>=mCount)
throw new IndexOutOfBoundsException();
// 正向查找
if (index <= mCount/2) {
DNode<T> node = mHead.next;
for (int i=0; i<index; i++)
node = node.next;
return node;
}
// 反向查找
DNode<T> rnode = mHead.prev;
int rindex = mCount - index -1;
for (int j=0; j<rindex; j++)
rnode = rnode.prev;
return rnode;
}
// 获取第index位置的节点的值
public T get(int index) {
return getNode(index).value;
}
// 获取第1个节点的值
public T getFirst() {
return getNode(0).value;
}
// 获取最后一个节点的值
public T getLast() {
return getNode(mCount-1).value;
}
// 将节点插入到第index位置之前
public void insert(int index, T t) {
if (index==0) {
DNode<T> node = new DNode<T>(t, mHead, mHead.next);
mHead.next.prev = node;
mHead.next = node;
mCount++;
return ;
}
DNode<T> inode = getNode(index);
DNode<T> tnode = new DNode<T>(t, inode.prev, inode);
inode.prev.next = tnode;
inode.next = tnode;
mCount++;
return ;
}
// 将节点插入第一个节点处。
public void insertFirst(T t) {
insert(0, t);
}
// 将节点追加到链表的末尾
public void appendLast(T t) {
DNode<T> node = new DNode<T>(t, mHead.prev, mHead);
mHead.prev.next = node;
mHead.prev = node;
mCount++;
}
// 删除index位置的节点
public void del(int index) {
DNode<T> inode = getNode(index);
inode.prev.next = inode.next;
inode.next.prev = inode.prev;
inode = null;
mCount--;
}
// 删除第一个节点
public void deleteFirst() {
del(0);
}
// 删除最后一个节点
public void deleteLast() {
del(mCount-1);
}
// 测试用例
public static void main(String[] args) {
int[] iarr = {10, 20, 30, 40};
System.out.println("\n----int_test----");
// 创建双向链表
DoubleLinkedListDemo<Integer> dlink = new DoubleLinkedListDemo<Integer>();
dlink.insert(0, 20); // 将 20 插入到第一个位置
dlink.appendLast(10); // 将 10 追加到链表末尾
dlink.insertFirst(30); // 将 30 插入到第一个位置
// 双向链表是否为空
System.out.printf("isEmpty()=%b\n", dlink.isEmpty());
// 双向链表的大小
System.out.printf("size()=%d\n", dlink.size());
// 打印出全部的节点
for (int i=0; i<dlink.size(); i++)
System.out.println("dlink("+i+")="+ dlink.get(i));
}
}
总结
总之,链表和数组的对比,在实际的软件开发中,不能仅仅利用复杂度分析就决定使用哪个数据结构来存储数据。要根据具体情况,权衡究竟是选择数组还是链表
- 数组简单易用,在实现上使用的是连续的内存空间,可以借助 CPU 的缓存机制,预读数组中的数据,所以访问效率更高。而链表在内存中并不是连续存储,所以对 CPU 缓存不友好,没办法有效预读。
- 数组的缺点是大小固定,一经声明就要占用整块连续内存空间。如果声明的数组过大,系统可能没有足够的连续内存空间分配给它,导致“内存不足(out of memory)”。如果声明的数组过小,则可能出现不够用的情况。这时只能再申请一个更大的内存空间,把原数组拷贝进去,非常费时。链表本身没有大小的限制,天然地支持动态扩容,我觉得这也是它与数组最大的区别。