在了解完什么是数据结构以后,让咱们一块儿来探索下数据结构中常见的一种—链表。面试
链表是数据结构之一,其中的数据呈线性排列。在链表中,数据的添加和删除都较为方便,就是访问比较耗费时间。算法
如上图所示就是链表的概念图,Blue、Yellow、Red 这 3 个字符串做为数据被存储于链表中,也就是数据域,每一个数据都有 1 个指针,即指针域,它指向下一个数据的内存地址,其中 Red 是最后 1 个数据,Red 的指针不指向任何位置,也就是为 NULL,指向 NULL 的指针一般被称为空指针。数组
在链表中,数据通常都是分散存储于内存中的,无须存储在连续空间内。数据结构
由于数据都是分散存储的,因此若是想要访问数据,只能从第 1 个数据开始,顺着指针的指向一一往下访问(这即是顺序访问)。好比,想要找到 Red 这一数据,就得从 Blue 开始访问,这以后,还要通过 Yellow,咱们才能找到 Red。ide
若是想要添加数据,只须要改变添加位置先后的指针指向就能够,很是简单。好比,在 Blue 和 Yellow 之间添加 Green。线程
首先将 Blue 的指针指向的位置变成 Green,而后再把 Green 的指针指向 Yellow,数据的添加就大功告成了。设计
数据的删除也同样,只要改变指针的指向就能够,好比删除 Yellow。3d
这时,只须要把 Green 指针指向的位置从 Yellow 变成 Red,删除就完成了。虽然 Yellow 自己还存储在内存中,可是无论从哪里都没法访问这个数据,因此也就没有特地去删除它的必要了。从此须要用到 Yellow 所在的存储空间时,只要用新数据覆盖掉就能够了。指针
那么对链表的操做所需的运行时间究竟是多少呢?在这里,咱们把链表中的数据量记成 n。访问数据时,咱们须要从链表头部开始查找(线性查找),若是目标数据在链表最后的话,须要的时间就是 O(n)。code
另外,添加数据只须要更改两个指针的指向,因此耗费的时间与 n 无关。若是已经到达了添加数据的位置,那么添加操做只需花费 O(1)的时间,删除数据一样也只需 O(1)的时间。
在对链表有了大概的认识之后,咱们用 Java 去实现属于本身的链表:
public class ListNode { int val; ListNode next; ListNode(int x) { val = x; } } class MyLinkedList { int size; /** * 哨兵节点做为伪头 */ ListNode head; public MyLinkedList() { size = 0; head = new ListNode(0); } /** * 获取链表第 index 个节点的值。若是索引是无效的,返回-1 * * @param index * @return */ public int get(int index) { // 若索引无效 if (index < 0 || index >= size) { return -1; } ListNode curr = head; // 从伪头节点开始,向前走 index+1 步 for (int i = 0; i < index + 1; ++i) { curr = curr.next; } return curr.val; } /** * 在头部插入节点 * * @param val */ public void addAtHead(int val) { addAtIndex(0, val); } /** * 在尾部插入节点 * * @param val */ public void addAtTail(int val) { addAtIndex(size, val); } /** * 在链表中的第 index 个节点前添加值为 val 的一个节点。若是 index 等于链表的长度时,节点将被添加到链表的末尾。若是 index 大于链表长度,节点将没法插入 * * @param index * @param val */ public void addAtIndex(int index, int val) { // 若是index大于长度,则不会插入该节点。 if (index > size) { return; } // 若是 index 为负,则该节点将插入列表的开头。 if (index < 0) { index = 0; } ++size; // 查找要添加的节点的前驱节点 ListNode pred = head; for (int i = 0; i < index; ++i) { pred = pred.next; } // 要添加的节点 ListNode toAdd = new ListNode(val); // 经过改变 next 来插入节点 toAdd.next = pred.next; pred.next = toAdd; } /** * 若是 index 是有效的,删除链表中的第 index 个节点 * * @param index */ public void deleteAtIndex(int index) { // 若是 index 无效,则不执行任何操做 if (index < 0 || index >= size) { return; } size--; // 找到要删除节点的前驱节点 ListNode pred = head; for (int i = 0; i < index; ++i) { pred = pred.next; } // 经过改变 next 来删除节点 pred.next = pred.next.next; } }
到这里,我相信你们应该对链表有了进一步的理解,你们能够用不一样的语言去设计实现下。
以上讲述的链表是最基本的一种链表,除此以外,还存在几种扩展方便的链表。
虽然上文中提到的链表在尾部没有指针,但咱们也能够在链表尾部使用指针,而且让它指向链表头部的数据,将链表变成环形,这即是循环链表,也叫环形链表。循环链表没有头和尾的概念,想要保存数量固定的最新数据时一般会使用这种链表。
循环链表
另外,以上提到的链表里的每一个数据都只有一个指针,但咱们能够把指针设定为两个,而且让它们分别指向先后数据,这就是双向链表。使用这种链表,不只能够从前日后,还能够从后往前遍历数据,十分方便。
可是,双向链表存在两个缺点:一是指针数的增长会致使存储空间需求增长;二是添加和删除数据时须要改变动多指针的指向。
双向链表
看完以后,相信你们都对链表和链表的基本操做有了必定的了解,还对循环链表和双向链表有了初步的认识,你们可使用本身喜欢的语言去设计实现下单向链表,有能力的话能够把循环链表和双向链表也实现下。
说完链表,固然不能忘记常常和链表同时出如今面试官口中的—数组,将在接下来的文章对其进行展开介绍。
参考
《个人第一本算法书》
完
●什么是数据结构?
●实现线程的方式到底有几种?
●Full GC 和 Minor GC,傻傻分不清楚
武培轩有帮助?在看,转发走一波喜欢做者