算法学习系列2——数组、链表、跳表的基本内容与特性2020-09-06

算法学习系列2——数组、链表、跳表的基本内容与特性

数组、链表、跳表的基本内容与特性

  • 算法学习系列2——数组、链表、跳表的基本内容与特性
  • 前言
    • Array 数组
    • 数组的基本操作
      • 增加元素
      • 删除元素
    • ArrayList
    • Linked List
    • 跳表
  • 总结

前言

 数组、链表与跳表是常见的,也是工作中经常使用到的数据结构,学好这三个数据结构将为自己未来的学习工作打下良好的基础,本文针对三者的实现与特性进行了分析说明,也提到了一些重点的面试常用考点内容。

Array 数组

数组是一种最简单的数据结构常见的声明有以下几种:

Java: int a[100] (= [?]);
Python: a = [];
JavaScript: let a = [1, 2, 3];

 当你声明一个数组的时候,计算会在内存中开辟连续的地址,计算机是通过内存管理器进行访问,访问任意一个元素,时间复杂度都是O(1),这也是数组的基本特性之一。

数组的基本操作

增加元素

 数组中如果增加一个元素的话,就必须把插入未知的元素及其后面的元素都要向后移动一位,这样导致了插入元素的时间复杂度为O(n)

删除元素

  跟插入元素内容类似,首先现将该位置的元素移出,然后将后续的元素向前移动一位,最后将最后一位空出来的位置内容置为空,能够唤醒Java的垃圾回收机制即可,或者手动管理内存的话,将数组size()减少即可。

ArrayList

  可以看到ArrayList的源码里针对于add()的操作;

public boolean add(E e)
{
	modCount++;
	//判断数组大小是否等于数据长度
	if (size == data.length)
		//保证数组长度大于数据长度
		ensureCapacity(size + 1);
		//最后在数组的末尾加入该元素,并且将size++
		data[size++] = e;
		return true;
}
public void add(int index, E element) {
 	//确保插入的位置小于等于当前数组长度,并且不小于0,否则抛出异常
 	rangeCheckForAdd(index);
 	//确保数组长度(size)加1之后足够存入下一个数据
 	//修改次数(modCount)标识加1,如果当前数组长度(size)加1后大于当前的数组长度,则调用grow(),增长数组
	ensureCapacityInternal(size + 1);  // Increments modCount!!
	//grow方法会将当前数组长度变为原来容量的1.5倍
	//确保有足够的容量之后,System.arraycopy 将需要插入的位置(index)后面的元素往后移动一位
    System.arraycopy(elementData, index, elementData, index + 1, size - index);
    //将新的数据内容存放到数组的指定位置(index)上
    elementData[index] = element;
    size++;
}
//将修改次数(modCount)自增1,判断是否需要扩充数组长度,判断条件就是用当前所需的数组最小长度与数组长度对比,如果大于0,则增长
private void ensureExplicitCapacity(int minCapacity) {
    modCount++;
    if (minCapacity - elementData.length > 0)
       grow(minCapacity);
}

//确保添加的元素有地方存储
private void ensureCapacityInternal(int minCapacity) {
    if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
        minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
    }
    ensureExplicitCapacity(minCapacity);
}

 可以看到数组在插入和删除数据频繁的时候,需要进行大量的数组copy操作,这样导致它并不高效,所欲如果大量涉及添加删除操作的时候,我们会考虑其他数据结构;

Linked List

  Linked List内部元素不管包含Value值,而且每一个元素都有一个Next指向下一个元素,每一个元素都是用class来定义的,包含的Value可以是class类型,而Next为指针;
  头指针用Head来表示,尾指针用Tail来表示,最后一个指针指向空,如果尾指针指向头部,该链表就是循环链表;

//指针为一前一后,所以Java中的链表是双向链表
Entry<T> next;
Entry<T> previous;

public boolean add(E e) {
    linkLast(e);
    return true;
}

void linkLast(E e) {
    final Node<E> l = last;
    final Node<E> newNode = new Node<>(l, e, null);
    last = newNode;
    f (l == null)
       first = newNode;
    else
       l.next = newNode;
     size++;
     modCount++;
}

private static class Node<E> {
    E item;
    Node<E> next;
    Node<E> prev;
    
    Node(Node<E> prev, E element, Node<E> next) {
        this.item = element;
        this.next = next;
        this.prev = prev;
    }
}

Linked List 增加节点

next指针
next指针
head
Node
Tail
next指针
next指针
head
Node
New Node
Tail

Linked List 删除节点

next指针
next指针
head
Node
Target Node
Tail
next指针
next指针
直接指向
head
Node
Target Node
Tail
next指针
next指针
head
Node
Tail

 链表中增删数据没有涉及到相应的数据整体复制移动,所以效率比较高;但是访问列表任意元素的话,必须从头结点一次查询,直到找到对应元素,所以复杂度较高O(n)

Linked List 的时间复杂度
prepend: O(1)
append: O(1)
lookup: O(n)
insert: O(1)
delete: O(1)

Array 的时间复杂度
prepend: O(1)
append: O(1)
lookup: O(1)
insert: O(n)
delete: O(n)

跳表

  注意:调表本身只能用于有序情况下;
  调表对应的是二叉搜索中的平衡树和二分查找,是一种插入、删除、查找都是*
  优点:原理简单、容易实现、便于扩展、效率更高,如Redis、LevelDB中替代平衡树;

  1. 对于一个一维的数据结构来说,采用的优化方式作为直接的就是升维(空间换时间);
    对于链表增加第一级索引(指向next+1元素);
  2. 在第一级索引之上继续添加第二级索引,第二级索引指向next+3元素;
  3. 以此类推可以加入多级索引,这样类似于二分查找一样,锁定元素位置;
  4. n个元素的跳表,共有k级索引,n/2, n/4, n/8, …第k级的为n/2k = 2;
  5. 那么查找时就需要查询k次,可以通过换算得到k值为k = log(n)-1;
  6. 增肌或者删除的情况下,意味着要将所有的节点都更新一边,那么复杂度就是O(log n);
  7. 因为每一层的节点数越来越少,可以发现满足收敛条件,所以空间复杂度为O(n);

总结

以上就是今天要讲的内容,本文仅仅简单介绍了链表、数组、跳表三种数据结构的原理和实现,分析三者的时间及空间复杂度;

你可能感兴趣的:(算法系列,链表,算法,数据结构,java)