链表

概念

链表的插入,只需要上一个节点的指针指向这个新增的节点,新增的节点指向下一个节点。
删除类似操作。

链表当前节点只知道下个节点是谁,也并非连续存储,随机访问元素时需要将整个链表遍历来查找元素。

添加和删除时间复杂度:O(1),随机查询时间复杂度:O(n)

单链表

单链表有头结点和尾结点。头结点用来记录链表的基地址,尾结点指向空地址null。


循环链表

跟单链表的区别就在于尾结点,单链表的尾结点指针指向空地址,表示这就是最后的结点了。而循环链表的尾结点指针是指向链表的头结点。
当要处理的数据具有环型结构特点时,就特别适合采用循环链表。

双向链表

双向链表需要额外的两个空间来存储后继结点和前驱结点的地址。所以,如果存储同样多的数据,双向链表要比单链表占用更多的内存空间。虽然两个指针比较浪费存储空间,但可以支持双向遍历,这样也带来了双向链表操作的灵活性。


跳表

思路:升维思想,空间换时间



如果要插入一个值是4的新节点,就不用8,7,6,5,3这样的查询.可以7,5,3这样的查询。
这是一层节点,还可以继续扩充成多层节点,提取的极限就是同一层就有两个节点的时候。
这种方式随机访问的时间复杂度降到了O(logn)
空间复杂度O(n)

链表的优缺点

优点:
链表本身没有大小的限制,天然地支持动态扩容。
缺点:
链表在内存中并不是连续存储,所以对CPU缓存不友好,没办法有效预读。
对链表进行频繁的插入、删除操作,还会导致频繁的内存申请和释放,容易造成内存碎片,有可能会导致频繁的 GC。
链表中的每个结点都需要消耗额外的存储空间去存储一份指向下一个结点的指针,所以内存消耗会翻倍。

不同链表比较

链表删除

  • 删除结点中“值等于某个给定值”的结点;
  • 删除给定指针指向的结点。

虽然单纯的删除时间复杂度是O(1),但是要查找删除的元素是个比较麻烦的问题,链表的随机访问元素时间复杂度是O(n)。
第一种情况,需要从头开始遍历链表来找到值等于给定值的结点。
第二种情况,我们已经找到了要删除的结点,但是删除某个结点q需要知道其前驱结点,进行尾结点指向改变来控制删除操作。这种情况单链表不知道前驱节点,查询时间复杂度是O(n),而对于双向链表时间复杂度是O(1)。
插入也是一致。

有序列表的查询

对于一个有序链表,双向链表的按值查询的效率也要比单链表高一些。因为,我们可以记录上次查找的位置p,每次查询时,根据要查找的值与p的大小关系,决定是往前还是往后查找,所以平均只需要查找一半的数据。

如何选取用单向链表还是双向链表

当内存空间充足的时候,如果我们更加追求代码的执行速度,我们就可以选择空间复杂度相对较高、但时间复杂度相对很低的算法或者数据结构。相反,如果内存比较紧缺,比如代码跑在手机或者单片机上,这个时候,就要反过来用时间换空间的设计思路。
使用缓存就是空间换时间的设计思想。

如何写链表代码

理解指针或引用的含义

p->next=q。这行代码是说,p结点中的next指针存储了q结点的内存地址。
p->next=p->next->next。这行代码表示,p结点的next指针存储了p结点的下下一个结点的内存地址。

防止指针丢失的情况

指针p指向节点A,需求想要在节点A,B中插入节点X。

p->next = x;  // 将p的next指针指向x结点;
x->next = p->next;  // 将x的结点的next指针指向b结点;

这种写法是错误的,因为第一行代码,P节点下一个节点位置已经指向到x,不再指向B了。所以第二行变成了X节点指向了自己,链表从此断了。
正确写法是1和2颠倒过来,先把x的下一个节点指向B,再把A的下一个节点指向x。

如果是删除,注意C的话要释放内存,防止内存泄漏。

特殊节点的添加和删除

1.正常节点的添加

// 新建的节点尾节点指向p的下一个节点 
new_node->next = p->next;
// p的尾节点指向新节点
p->next = new_node;

2.添加头节点

if (head == null) {
  head = new_node;
}

3.正常节点的删除

p->next = p->next->next;

4.删除最后一个节点

if (head->next == null) {
   head = null;
}

这种方式很容易出错,需要引入哨兵节点,在任何时候,不管链表是不是空,head指针都会一直指向这个哨兵结点。我们也把这种有哨兵结点的链表叫带头链表。相反,没有哨兵结点的链表就叫作不带头链表。哨兵结点是不存储数据的。


// 在数组a中,查找key,返回key所在的位置
// 其中,n表示数组a的长度
// 我举2个例子,你可以拿例子走一下代码
// a = {4, 2, 3, 5, 9, 6}  n=6 key = 7
// a = {4, 2, 3, 5, 9, 6}  n=6 key = 6
int find(char* a, int n, char key) {
  if(a == null || n <= 0) {
    return -1;
  }
  
  // 这里因为要将a[n-1]的值替换成key,所以要特殊处理这个值
  if (a[n-1] == key) {
    return n-1;
  }
  
  // 把a[n-1]的值临时保存在变量tmp中,以便之后恢复。tmp=6。
  // 之所以这样做的目的是:希望find()代码不要改变a数组中的内容
  char tmp = a[n-1];
  // 把key的值放到a[n-1]中,此时a = {4, 2, 3, 5, 9, 7}
  a[n-1] = key;
  
  int i = 0;
  // while 循环比起代码一,少了i<n这个比较操作
  while (a[i] != key) {
    ++i;
  }
  
  // 恢复a[n-1]原来的值,此时a= {4, 2, 3, 5, 9, 6}
  a[n-1] = tmp;
  
  if (i == n-1) {
    // 如果i == n-1说明,在0...n-2之间都没有key,所以返回-1
    return -1;
  } else {
    // 否则,返回i,就是等于key值的元素的下标
    return i;
  }
}

重点留意边界条件处理

如果链表为空时,代码是否能正常工作?
如果链表只包含一个结点时,代码是否能正常工作?
如果链表只包含两个结点时,代码是否能正常工作?
代码逻辑在处理头结点和尾结点的时候,是否能正常工作?

LinkedList源码

底层数据结构双向链表。链表中的元素是Node。

private static class Node<E> {
    E item;
    LinkedList.Node<E> next;
    LinkedList.Node<E> prev;

    Node(LinkedList.Node<E> var1, E var2, LinkedList.Node<E> var3) {
        this.item = var2;
        this.next = var3;
        this.prev = var1;
    }
}

增加

可以从头部增加,也可以从尾部增加。默认add从尾部增加。
1.从头部增加节点

private void linkFirst(E var1) {
    LinkedList.Node var2 = this.first;
    // 创建的节点,前一个是null,当前节点是var1,尾节点是var2
    LinkedList.Node var3 = new LinkedList.Node((LinkedList.Node)null, var1, var2);
    // 新建的节点作为头节点
    this.first = var3;
    // 如果之前没有头节点,意味着之前链表为空,当前添加元素,头节点,尾节点都是同一个节点
    if (var2 == null) {
        this.last = var3;
    } else {
        var2.prev = var3;
    }

    ++this.size;
    ++this.modCount;
}

2.从尾部增加节点

void linkLast(E var1) {
    LinkedList.Node var2 = this.last;
    // 当前节点上一个节点是之前链表的尾节点,下一个节点是null
    LinkedList.Node var3 = new LinkedList.Node(var2, var1, (LinkedList.Node)null);
    this.last = var3;

    if (var2 == null) {
        this.first = var3;
    } else {
        var2.next = var3;
    }

    ++this.size;
    ++this.modCount;
}

删除

可以选择从头部删除,也可以选择从尾部删除,给定index删除节点,删除给定节点。删除操作会把节点的值,前后指向节点都置为null,帮助GC进行回收。
remove(Object o)
添加没有特殊判断,可以添加null,删除也可以删除null。
从头节点向尾节点遍历,删除null,判断节点数据是否为null。
删除其余元素,通过equals判断值是否相同。

E unlink(Node<E> x) {
    final E element = x.item;
    final Node<E> next = x.next;
    final Node<E> prev = x.prev;

    // 删除的节点就是头节点
    if (prev == null) {
        // 下一个节点作为头节点
        first = next;
    } else {
        // 前一个节点的尾指针指向下一个节点
        prev.next = next;
        // 当前节点的头节点置空
        x.prev = null;
    }

    if (next == null) {
        // 上一个节点置空
        last = prev;
    } else {
        next.prev = prev;
        // 当前节点的尾节点为空
        x.next = null;
    }
    // 删除的节点数据为空
    x.item = null;
    size--;
    modCount++;
    return element;
}

remove(int index)
需要找到这个index对应的节点信息,通过上面的删除方式删除节点。

Node<E> node(int index) {
    // 二分
    if (index < (size >> 1)) {
        // 获取到头节点,根据index一直向下获取
        Node<E> x = first;
        for (int i = 0; i < index; i++)
            x = x.next;
        return x;
    } else {
        // 获取到尾节点根据index向上获取
        Node<E> x = last;
        for (int i = size - 1; i > index; i--)
            x = x.prev;
        return x;
    }
}

remove()
相当于删除链表头节点元素

private E unlinkFirst(Node<E> f) {
    // 获取要删除节点的下个条目和当前条目
    final E element = f.item;
    final Node<E> next = f.next;
    // 清空要删除节点的内容,把它的尾指针指空
    f.item = null;
    f.next = null; // help GC
    // 头节点的下个节点成为头节点
    first = next;
    // 当个链表只有一个节点
    if (next == null)
        last = null;
    else
        // 头节点的下个节点头指针指向null,它要成为头节点
        next.prev = null;
    size--;
    modCount++;
    return element;
}

迭代器

从头到尾方式迭代。

hashNext方法

public boolean hasNext() {
    // 下一个节点的索引小于链表的大小
    return this.nextIndex < LinkedList.this.size;
}

next方法

public E next() {
    this.checkForComodification();
    if (!this.hasNext()) {
        throw new NoSuchElementException();
    } else {
        // 当前要删除的节点
        this.lastReturned = this.next;
        // next是下一个节点了,为下次迭代做准备
        this.next = this.next.next;
        ++this.nextIndex;
        return this.lastReturned.item;
    }
}

remove方法

public void remove() {
    this.checkForComodification();
    if (this.lastReturned == null) {
        throw new IllegalStateException();
    } else {
        LinkedList.Node var1 = this.lastReturned.next;
        // 删除当前节点
        LinkedList.this.unlink(this.lastReturned);
        if (this.next == this.lastReturned) {
            this.next = var1;
        } else {
            --this.nextIndex;
        }

        this.lastReturned = null;
        ++this.expectedModCount;
    }
}
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 220,192评论 6 511
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,858评论 3 396
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 166,517评论 0 357
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 59,148评论 1 295
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 68,162评论 6 397
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,905评论 1 308
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,537评论 3 420
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,439评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,956评论 1 319
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 38,083评论 3 340
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,218评论 1 352
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,899评论 5 347
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,565评论 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,093评论 0 23
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,201评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,539评论 3 375
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,215评论 2 358