数据结构-数组、链表、跳表

第三课:数组、链表、跳表
一、数组Array List
数组的底层实现原理
内存管理器:
 每次申请数组,计算机实际上是在内存中开辟一段连续的地址,每一个地址直接可以通过内存管理器进行访问
数组的优缺点
优点:
可以随机的访问任何一个元素,访问速度很快

缺点:
对数组元素进行增加或者删除的时候效率很低
数组增加元素

在这里插入图片描述

在这里插入图片描述

当我们想把这样一个数组中在索引为3的位置上插入新元素D,那么我们需要先把EFG依次往后挪一个位置,给D腾出一个空间,D才能进行插入操作,因此插入最坏的情况下的时间复杂度为O(n),最好的情况下的时间复杂度为O(1)
ArrayListadd()的源码:
//在数组的最后一个元素添加新的元素
    public boolean add(E e) {
    	//判断数组有没有满
        ensureCapacityInternal(size + 1);  // Increments modCount!!
        elementData[size++] = e;
        return true;
    }

//在数组的指定位置中添加新的元素
	public void add(int index, E element) {
	//判断传进来的索引是否会导致数组越界
        rangeCheckForAdd(index);

        ensureCapacityInternal(size + 1);  // Increments modCount!!
        //arraycopy():拷贝操作,把原地址的起点位置拷贝到目标地址的起点位置
        //elementData, index:数组的原位置;elementData, index + 1:目标地址;size - index:长度,后半部分要挪动的部分
        System.arraycopy(elementData, index, elementData, index + 1,size - index);
        elementData[index] = element;
        size++;
    }

因此,如果对数组进行增加操作,会涉及到非常多的array copy的操作,时间复杂度以及空间复杂度就会偏低

数组删除元素

在这里插入图片描述

在这里插入图片描述

此时若想删除元素Z,则我们需要:
1.Z元素取出,挪出数组
2.Z元素后的所有元素都向前移动一位
ArrayListadd()的源码:
/**
     * Appends the specified element to the end of this list.
     *
     * @param e element to be appended to this list
     * @return <tt>true</tt> (as specified by {@link Collection#add})
     */
    public boolean add(E e) {
        ensureCapacityInternal(size + 1);  // Increments modCount!!
        elementData[size++] = e;
        return true;
    }
数组的增删改查操作的时间复杂度
prepend:头结点处增加元素

append:尾节点处增加元素
prependO(1)
appendO(1)
lookupO(1)
insertO(n)
deleteO(n)
二、链表Linked List
链表就是为了解决上述数组存在的问题,提升增加、删除元素效率,在增加和删除操作比较多的情况,优先考虑使用链表
链表中的每个元素一般用class去定义,一般称为node,节点类里面有两个成员变量:
1.value:存放该节点的数值
2.next:指针,指向其下一个元素

如果只有一个指针,则称为单链表;如果往前面也加一个指针,则称为先前指针previous,则该链表称为双向链表,既能向后面走,也能向前面走:头指针用head表示,尾指针用tail表示(一般而言,最后一个元素尾指针指向空);最后一个元素尾指针指向head,此时则变为循环链表

在这里插入图片描述

链表的定义
//链表的简单定义
class LinkedList {
	Node head; // head of list

	/* Linked list Node*/
	class Node {
		int data;
		Node next;

		// Constructor to create a new node
		// Next is by default initialized
		// as null
		Node(int d) { data = d; }
	}
}

Java里的链表定义

在这里插入图片描述

定义的是标准的双向链表结构

链表增加结点

1.原始链表如下图所示
在这里插入图片描述

2.现在有个新的结点欲添加进来
在这里插入图片描述

3.把前一个结点的next指针指向新结点,将新结点的next指针指向原来的后一个结点
在这里插入图片描述

在链表中进行增加操作只需要O(1)的复杂度

链表删除结点

链表删除节点就是刚刚增加节点的逆操作

1.原始链表如下图所示,其中Target Node为欲删除的结点
在这里插入图片描述

2.将Target Node的前躯的结点的next指向Target Node的后继结点
在这里插入图片描述

3.删除操作完成
在这里插入图片描述

链表删除/增加结点的特点
	不管是删除操作还是增加操作都没有引起整个链表的群移操作,操作的过程中也没有复制元素,因此链表删除/增加元素的操作效率十分高,为O(1)
    
	但是这也导致了另一个缺点的暴露:要访问链表中任意一个元素就不再像数组一样简便,必须从头结点依次遍历,直到达到欲访问结点的位置,因此链表查找的时间复杂度为O(n)
链表的增删改查操作的时间复杂度
prependO(1)
appendO(1)
lookupO(n)
insertO(1)
deleteO(1)

Tips:

数组、链表的增加删除查找操作的时间复杂度要非常的清楚!

三、跳表SkipList
跳表理解即可,不需要深究

1.对链表进行了优化而产生的SkipList
2.主要在Redis里使用

优化链表的思想:升维(空间换时间)
    将链表从一维结构升为二维结构
    
为了提高链表线性查找的效率,SkipList增加了索引

一级索引:
第一个指针指向头指针
第二个指针指向next+1
    ...
原始链表的next每次都是只往前走一步,但是一次索引每次都向前走两步,因此加了一级索引之后,访问速度就是原来的两倍

为了更快,我们可以使用二级索引,原始链表的next每次都是只往前走一步,一次索引每次都向前走两步,二次索引每次都向前走四步,因此加了二级索引之后,访问速度就是原来的四倍
	...
以此类推,增加多级索引:增加log2n个级索引

非常重要的思想:

1.升维

2.空间换时间

一级索引:
在这里插入图片描述

二级索引:
在这里插入图片描述

多级索引:
在这里插入图片描述

跳表查询的时间复杂度
第k级索引结点的个数是n/(2^k)
    
假如索引有m级,最高级的索引有2个结点,n/(2^m) = 2,因此m = log2(n)-1
    
在跳表中查询数据的时间复杂度是O(logn)
跳表的空间复杂度
跳表的空间复杂度是O(n)
实际应用中跳表的形态
由于实际应用中,索引会随着元素的增加和删除发生变化,有些索引在个别地方跨m步,在其他地方跨n步,不是规整的索引结构

跳表的维护成本较高,每增加/删除元素一次就得更新一次索引,因此跳表增加/删除操作的时间复杂度是O(logn)

在这里插入图片描述

链表及跳表的实际应用场景
1.链表:LRU Cache
https://www.jianshu.com/p/b 1ab4a170c3c
https://leetcode-cn.com/problems/lru-cache

2.跳表:Redis
https://redisbook.readthedocs.io/en/latest/internal-datastruct/skijplist.html
https://www.zhihu.com/question/20202931

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值