我们刚接触数据结构最基础的数据结构那就是数组,但是数组在创建时大小就要被固定,数组的存储地址要是连续的等等不好的地方,当然数组也有他的好处,查询速度快等等我们这篇博客以数组和链表的不同开始讲解链表,最后再来剖析一下JDK中实现的链表LinkedList的源码,通过学习源码进一步提升。
有兴趣朋友可以看看我的数组的博客。
数组和链表
- 数组有‘1好2不好’
- 好是,数组的内存是绝对连续的,因此数组的随机访问操作非常的快,时间复杂度是O(1) O(1)O(1),为常量时间,例如arr[20]和arr[2000]的访问花费的时间是一样的。
- 不好是 :
a)数组在定义的时候,必须指定其大小,实际应用中,当数组元素满了以后,要进行扩容,扩容的代码虽然简单,如arr = Arrays.copyOf(arr, arr.length*2)就能够达到2倍扩容的效果,但是当数组的元素数量比较大的话,扩容需要经过开辟更大块内存,拷贝数据,GC回收原来的旧内存等步骤,其效率就比较低了,扩容花费的时间也长了。当扩容一段时间后,数组内存空间特别大,但是随着删除操作,最后只有少量的有效元素(如10000个数组元素空间,却只存储了10个有效元素),内存就被浪费了,这时候就得缩容(如ArrayList数组集合的缩容实现),其效率也比较低。
b)在数组中,插入元素或者删除元素,都需要经过大量数据的移动。插入操作会引起插入点后面的元素都向后进行移动;删除操作会引起删除点后面的元素都向前进行移动,这两个操作的时间花费都是O(n) O(n)O(n),是线性时间,说明随着数组的元素数量越多,插入删除操作的效率越低。
- 相对于数组,那么链表就有‘1不好2好’
- 2好是:
a)链表的每一个元素节点都是独立new出来的,也就是说链表中所有元素节点内存并不是连续的,而是上一个节点记录了下一个节点的地址,这样内存的使用效率就非常的高,在存储数据量比较大的时候,不需要大片连续的内存空间,因此只要当前JVM可用内存足够大,链表就可以无限生成新的节点,不存在大块内存开辟,大量数据拷贝,GC回收旧内存的内存扩容问题,这一点要比数组优秀!
b)在链表中插入数据,只需要生成新的节点接入链表中就可以,不涉及其它数据节点的移动;在链表中删除数据,只需要把待删除节点从原链表中卸载下来就可以,也不涉及其它数据节点的移动,也就是数据插入和删除的时间复杂度是O(1) O(1)O(1),常量时间,速度很快(这个指的是插入删除操作本身花费的时间,往往在链表中做插入和删除操作的时候,首先会遍历链表,找到合适的位置,那么链表搜索的时间是O(n) O(n)O(n),为线性时间)。
-
1不好是,因为链表中节点的内存不是连续的,所以就不能像数组那样支持元素的随机访问(就是通过指定下标,访问对应的元素的值),当在链表中访问一个元素节点时,总是要从头节点开始,一个个往后遍历,链表元素越多,遍历的时间就越长,因此链表遍历搜索的时间是O(n) O(n)O(n),为线性时间,没有数组随机访问O(1) O(1)O(1)的效率好。
-
结论:所以一般插入删除操作多,用链表;随机访问多,用数组。当然还需要具体情况具体分析。数组和链表各操作时间。
链表的实现
单链表
public class SingleLinkedList<T> {
HeadNode <T> headNode;
public SingleLinkedList() {
this.headNode = new HeadNode<>(null,null,0);
}
/**
* 头插法,头结点后插入数据
* @param val
*/
public void insertHead(T val){
Node<T> newNode=new Node<>(val,this.headNode.next);//将新节点的下一个设置为链表第二个节点
this.headNode.next=newNode;
this.headNode.count++;
}
/**
* 尾插法,链表尾部插入数据
* @param val
*/
public void insertTail(T val){
Node node=this.headNode;
while (node.next != null){
node=node.next;
}
node.next=new Node(val,null);
this.headNode.count++;
}
/**
* 展示链表中所有的节点
*/
public void show(){
Node node=this.headNode.next;
while (node!=null){
System.out.println(node.data);
node=node.next;
}
}
/**
* 删除指定位置的节点
* @param val
*/
public void remove(T val){
Node<T> pre = headNode;
Node<T> cur = headNode.next;
while(cur != null){
if(cur.data == val){
// val节点的删除
pre.next = cur.next;
cur = pre.next; // 重置cur,继续向后删除链表中所有值为val的节点
this.headNode.count -= 1; // 更新头节点中链表节点的个数
} else {
pre = cur;
cur = cur.next;
}
}
}
/**
* 单链表的节点类型
* @param <T>
*/
static class Node<T>{
T data;//链表中存储的数据
Node<T> next;//下一个节点
public Node(T data, Node<T> next) {
this.data = data;
this.next = next;
}
}
/**
* 链表的头结点
* @param <T>
*/
static class HeadNode<T> extends Node<T>{
int count;//链表中数据的个数
public HeadNode(T data, Node<T> next,int count) {
super(data, next);
this.count=count;
}
}
}
单向循环链表
public class CircleLinkList<T> {
int size;//链表长度
Node<T> headNode;//头结点
Node<T> tailNode;//尾节点
public CircleLinkList() {
this.size = 0;
this.headNode = null;
this.tailNode = null;
}
/**
* 头部插入数据
* @param val
*/
public void addHead(T val){
Node node=new Node(val,null);
if (size<=0){//链表无元素
node.next=node;
tailNode=headNode=node;
size++;
}else {
tailNode.next=node;
node.next=headNode;
headNode=node;
size++;
}
}
/**
* 尾部插入数据
* @param val
*/
public void addTail(T val){
Node node=new Node(val,null);
if (size<=0){
node.next=node;
tailNode=headNode=node;
size++;
}else {
tailNode.next=node;
node.next=headNode;
tailNode=node;
size++;
}
}
public void disPlay(){
Node node=headNode;
while (node.next!=headNode){
System.out.println(node.data);
node=node.next;
}
System.out.println(node.data);
}
//节点类型
static class Node<T>{
T data;
Node<T> next;
public Node(T data, Node<T> next) {
this.data = data;
this.next = next;
}
}
}
双向链表
我们这里先自己来实现双向链表,在JDK中LinkedList实现就是双向链表,之后我们会进行源码剖析。
public class TwoWayLinkList<T> {
int size;//节点的个数
Node headnode;//头结点
public TwoWayLinkList() {
this.size = 0;
this.headnode = new Node(null, null, null);
}
/**
* 头插数据
*
* @param val
*/
public void addHead(T val) {
Node<T> node = new Node<>(val, headnode, headnode.nextnode);
this.headnode.nextnode = node;
if (node.nextnode != null) {
node.nextnode.prenode = node;
}
size++;
}
public void addTail(T val) {
Node tmpnode = this.headnode;
while (tmpnode.nextnode != null) {
tmpnode = tmpnode.nextnode;
}
tmpnode.nextnode = new Node<>(val, tmpnode, null);
size++;
}
/**
* 打印整个链表
*/
public void disPlayList() {
Node node = this.headnode.nextnode;
while (node != null) {
System.out.println(node.data);
node = node.nextnode;
}
}
/**
* 节点
*
* @param <T>
*/
static class Node<T> {
T data;
Node<T> prenode;
Node<T> nextnode;
public Node(T data, Node<T> prenode, Node<T> nextnode) {
this.data = data;
this.prenode = prenode;
this.nextnode = nextnode;
}
}
}
反转链表的三种方法
- 下面是基于单链表的的逆置方法
- 反转链表方法一:将这个链表每一个节点的next改为前一个节点,最后把headnode.next重置
/**
* 反转链表方法一:将这个链表每一个节点的next改为前一个节点,最后把headnode.next重置
*/
public void reverse2(){
Node pre=null;
Node curr=headNode.next;
Node tmp;
while (curr!=null){
tmp=curr.next;
//当前节点的下一个节点,改为前一个节点
curr.next=pre;
//移动pre和curr
pre=curr;
curr=tmp;
}
headNode.next=pre;
}
- 把当前链表的下一个节点pCur插入到头结点dummy的下一个节点中,就地反转。
把当前链表的下一个节点pCur插入到头结点dummy的下一个节点中,就地反转。
dummy->1->2->3->4->5的就地反转过程:
dummy->2->1->3->4->5
dummy->3->2->1->4->5
dummy->4>-3->2->1->5
dummy->5->4->3->2->1
public ListNode reverseList1(ListNode head) {
3 if (head == null)
4 return head;
5 ListNode dummy = new ListNode(-1);
6 dummy.next = head;
7 ListNode prev = dummy.next;
8 ListNode pCur = prev.next;
9 while (pCur != null) {
10 prev.next = pCur.next;
11 pCur.next = dummy.next;
12 dummy.next = pCur;
13 pCur = prev.next;
14 }
15 return dummy.next;
16 }
- 创建一个新链表不断使用头插法
判断链表是否有环
/**
* 判断链表是否有环,如果有,返回入环节点的值,没有环,返回null
* @return
*/
public T getLinkCircleVal(){
Entry<T> slow = this.head.next;
Entry<T> fast = this.head.next;
// 使用快慢指针解决该问题
while(fast != null && fast.next != null){
slow = slow.next;
fast = fast.next.next;
if(slow == fast){
break;
}
}
if(fast == null){
return null;
} else {
/**
* fast从第一个节点开始走,slow从快慢指针相交的地方开始走,
* 它们相遇的时候,就是环的入口节点
*/
fast = this.head.next;
while(fast != slow){
fast = fast.next;
slow = slow.next;
}
return slow.data;
}
}
合并两个单链表
/**
* 合并两个有序的单链表
* @param link
*/
public void merge(Link<T> link){
Entry<T> p = this.head;
Entry<T> p1 = this.head.next;
Entry<T> p2 = link.head.next;
// 比较p1和p2节点的值,把值小的节点挂在p的后面
while(p1 != null && p2 != null){
if(p1.data.compareTo(p2.data) >= 0){
p.next = p2;
p2 = p2.next;
} else {
p.next = p1;
p1 = p1.next;
}
p = p.next;
}
if(p1 != null){ // 链表1还有剩余节点
p.next = p1;
}
if(p2 != null){ // 链表2还有剩余节点
p.next = p2;
}
}
LinkedList源码剖析
我们看LinkedList几个需要注意的点,其他方面它的实现和我上面的基本相似。
- Node节点,我们能够看到LinkedList是双向链表。
- 当我们调add方法添加元素默认是尾插法
- get方法获取指定位置的元素,我们看到它是用了二分查找的思想,