知识的学习在于点滴记录,坚持不懈;知识的学习要有深度和广度,不能只流于表面,坐井观天;知识要善于总结,不仅能够理解,更知道如何表达!
在大家接触链表之前,都大量的使用过数组,数组有‘1好2不好’,怎么解释呢?
1好是,数组的内存是绝对连续的,因此数组的随机访问操作非常的快,时间复杂度是 O ( 1 ) O(1) O(1),为常量时间,例如arr[20]和arr[2000]的访问花费的时间是一样的。
2不好是 :
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)的效率好。
结论:所以一般插入删除操作多,用链表;随机访问多,用数组。当然还需要具体情况具体分析。数组和链表各操作时间复杂度对比如下:
名称 | 插入 | 删除 | 查找&搜搜 | 随机访问 |
---|---|---|---|---|
数组 | O ( n ) O(n) O(n) | O ( n ) O(n) O(n) | O ( n ) O(n) O(n) | O ( 1 ) O(1) O(1) |
单链表 | O ( 1 ) O(1) O(1) | O ( 1 ) O(1) O(1) | O ( n ) O(n) O(n) | 不支持 |
/**
* 带头节点的单链表的实现
* @param
*/
class Link<T extends Comparable<T>>{
/**
* 指向单链表的头节点,其地址域中记录了链表第一个节点的地址
*/
HeadEntry<T> head;
/**
* 初始化链表,生成头节点被head指向
*/
public Link(){
this.head = new HeadEntry<>(0, null, null);
}
/**
* 单链表的头插法
* @param val
*/
public void insertHead(T val){
Entry<T> node = new Entry<>(val, this.head.next);
this.head.next = node;
this.head.cnt += 1; // 更新头节点中链表节点的个数
}
/**
* 单链表的尾插法
* @param val
*/
public void insertTail(T val){
Entry<T> node = head;
while(node.next != null){
node = node.next;
}
node.next = new Entry<>(val, null);
this.head.cnt += 1; // 更新头节点中链表节点的个数
}
/**
* 单链表中删除所有值是val的节点
* @param val
*/
public void remove(T val){
Entry<T> pre = head;
Entry<T> cur = head.next;
while(cur != null){
if(cur.data == val){
// val节点的删除
pre.next = cur.next;
cur = pre.next; // 重置cur,继续向后删除链表中所有值为val的节点
this.head.cnt -= 1; // 更新头节点中链表节点的个数
} else {
pre = cur;
cur = cur.next;
}
}
}
/**
* 打印单链表的所有节点元素的值
*/
public void show(){
Entry<T> cur = this.head.next;
while (cur != null) {
System.out.print(cur.data + " ");
cur = cur.next;
}
System.out.println();
}
/**
* 获取链表节点的个数
* @return
*/
public int size() {
return this.head.cnt;
}
/**
* 单链表的节点类型
* @param
*/
static class Entry<T>{
T data; // 链表节点的数据域
Entry<T> next; // 下一个节点的地址
public Entry(T data, Entry<T> next) {
this.data = data;
this.next = next;
}
}
/**
* 头节点的类型,添加了int cnt成员变量,记录链表中节点的个数
* @param
*/
static class HeadEntry<T> extends Entry<T>{
int cnt; // 用来记录节点的个数
public HeadEntry(int cnt, T data, Entry<T> next) {
super(data, next);
this.cnt = cnt;
}
}
}
/**
* 描述: 单链表测试
* @Author administrator
* @Date 5/11
*/
public class LinkTestUnit {
@Test
public void test01(){
Random rd = new Random();
Link<Integer> link1 = new Link<>();
for (int i = 0; i < 10; i++) {
link1.insertHead(rd.nextInt(20)+1); // 随机[1,20]间的元素值
}
for (int i = 0; i < 10; i++) {
link1.insertTail(rd.nextInt(20)+1); // 随机[1,20]间的元素值
}
link1.show();
System.out.println(link1.size());
link1.remove(15);
link1.show();
System.out.println(link1.size());
}
}
注意:上面链表尾插法insertTail,每次插入元素都要先从头节点开始,一个个遍历到末尾节点进行添加元素,时间复杂度是 O ( n ) O(n) O(n),解决办法是可以给链表添加一个成员变量tail,让tail永远指向末尾节点,然后尾插法把新节点直接插入到tail后面,再让tail指向新的末尾节点就可以了。
/**
* 逆置单链表,思想是从链表第二个节点开始采用头插法重新链接节点
*/
public void reverse(){
if(this.head.next == null){
return;
}
// 从链表第二个节点开始逆置
Entry<T> cur = this.head.next.next;
this.head.next.next = null;
Entry<T> post = null;
while(cur != null){
post = cur.next;
cur.next = head.next;
head.next = cur;
cur = post;
}
}
/**
* 获取倒数第K个单链表节点的值
* @param k
* @return
*/
public T getLastK(int k){
Entry<T> cur1 = this.head.next;
Entry<T> cur2 = this.head;
// cur1指向第一个节点,cur2指向正数第k个节点
for (int i = 0; i < k; i++) {
cur2 = cur2.next;
if(cur2 == null){
return null;
}
}
// 当cur2到达末尾节点时,cur1指向的就是倒数第k个节点
while(cur2.next != null){
cur1 = cur1.next;
cur2 = cur2.next;
}
return cur1.data;
}
/**
* 判断链表是否有环,如果有,返回入环节点的值,没有环,返回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;
}
}
/**
* 判断两个单链表是否相交,思路就是先获取两个链表的长度,得到长度的差size,
* 然后长链表先从头节点走size个,然后两个链表开始同时遍历,遇到相同的节点,说明
* 链表相交了,如果遍历到null,说明链表没有相交
* @param link
* @return
*/
public T isLinkIntersect(Link<T> link){
Entry<T> cur1 = this.head;
Entry<T> cur2 = link.head;
// 求两个链表的长度
int len1 = 0;
int len2 = 0;
while(cur1 != null) {
cur1 = cur1.next;
len1++;
}
while(cur2 != null) {
cur2 = cur2.next;
len2++;
}
// 重置指针,找是否存在相交节点
cur1 = this.head;
cur2 = link.head;
if(len1 > len2){ // 链表1比较长
int size = len1 - len2;
for(int i=0; i<size; ++i){
cur1 = cur1.next;
}
} else if(len1 < len2){ // 链表2比较长
int size = len2 - len1;
for(int i=0; i<size; ++i){
cur2 = cur2.next;
}
}
while(cur1 != null && cur2 != null){
if(cur1 == cur2){ // 两个指针指向同一个节点,链表相交了
return cur1.data;
}
cur1 = cur1.next;
cur2 = cur2.next;
}
return null;
}
/**
* 合并两个有序的单链表
* @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;
}
}
/**
* 双向链表的实现
* @param
*/
class Link<T extends Comparable<T>>{
/**
* 指向双向链表的头节点,其地址域中记录了链表第一个节点的地址
*/
private Entry<T> head;
/**
* 构造函数,生成head指向的头节点,头节点不存数据
*/
public Link(){
head = new Entry<>(null, null, null);
}
/**
* 双向链表的头插法
* @param val
*/
public void insertHead(T val){
Entry<T> node = new Entry<>(val, this.head.next, this.head);
this.head.next = node;
if(node.next != null){
node.next.pre = node;
}
}
/**
* 双链表的尾插法
* @param val
*/
public void insertTail(T val){
Entry<T> cur = this.head;
while(cur.next != null){
cur = cur.next;
}
cur.next = new Entry<>(val, null, cur);
}
/**
* 双向链表删除所有值为val的节点
* @param val
*/
public void remove(T val){
Entry<T> cur = this.head.next;
while(cur != null){
if(cur.data.compareTo(val) == 0){
cur.pre.next = cur.next;
if(cur.next != null){
cur.next.pre = cur.pre;
}
}
cur = cur.next;
}
}
/**
* 打印双向链表所有节点元素的值
*/
public void show(){
Entry<T> cur = this.head.next;
while(cur != null){
System.out.print(cur.data + " ");
cur = cur.next;
}
System.out.println();
}
/**
* 链表节点类型定义
* @param
*/
static class Entry<T>{
T data;
Entry<T> next; // 存储后一个节点的地址
Entry<T> pre; // 存储前一个节点的地址
public Entry(T data, Entry<T> next, Entry<T> pre) {
this.data = data;
this.next = next;
this.pre = pre;
}
}
}
/**
* 描述:
*
* @Author administrator
* @Date 5/11
*/
public class DoubleLinkTestUnit {
public static void main(String[] args) {
Random rand = new Random();
Link<Integer> link = new Link<>();
for (int i = 0; i < 10; i++) {
link.insertHead(rand.nextInt(20) + 1);
}
for (int i = 0; i < 10; i++) {
link.insertTail(rand.nextInt(20) + 1);
}
link.show();
link.remove(8);
link.show();
}
}
栈是一种先进后出,后进先出的线性表数据结构,实现虽然简单,但是其用途非常的广泛,很多问题的求解都需要依赖一个栈结构,比如递归转非递归代码实现,一般都会依赖栈,比如二叉树的前中后序遍历的非递归实现;还有逆波兰表达式的求解也需要依赖栈,等等。下面总结一下Java实现的顺序栈和链栈代码。
/**
* 顺序栈实现
* @param
*/
class SeqStack<T>{
// 存储栈的元素的数组
private T[] stack;
// top表示栈顶的位置
private int top;
public SeqStack(){
this(10);
}
public SeqStack(int size){
this.stack = (T[])new Object[size];
this.top = 0;
}
/**
* 入栈操作
* @param val
*/
public void push(T val){
if(full()){
// 栈如果满了,要进行内存2倍扩容
this.stack = Arrays.copyOf(this.stack,
this.stack.length*2);
}
this.stack[this.top] = val;
this.top++;
}
/**
* 出栈操作
*/
public void pop(){
if(empty())
return;
this.top--;
this.stack[this.top] = null;
}
/**
* 返回栈顶元素
* @return
*/
public T top(){
return this.stack[this.top - 1];
}
/**
* 判断栈满
* @return
*/
public boolean full(){
return this.top == this.stack.length;
}
/**
* 判断栈空
* @return
*/
public boolean empty(){
return this.top == 0;
}
}
/**
* 描述:链式栈结构
*/
public class LinkStack<T> {
// top指向头节点,头节点的后面就是栈顶节点
private Entry<T> top;
public LinkStack(){
this.top = new Entry<>(null, null);
}
/**
* 入栈操作
* @param val
*/
public void push(T val){
Entry<T> node = new Entry<>(val, this.top.next);
this.top.next = node;
}
/**
* 出栈操作
* @return
*/
public T pop(){
T val = null;
if(this.top.next != null){
val = this.top.next.data;
this.top.next = this.top.next.next;
}
return val;
}
/**
* 查看栈顶元素
* @return
*/
public T peek(){
T val = null;
if(this.top.next != null){
val = this.top.next.data;
}
return val;
}
/**
* 判断栈空
* @return
*/
public boolean isEmpty(){
return this.top.next == null;
}
/**
* 节点类型定义
* @param
*/
static class Entry<T>{
T data;
Entry<T> next;
public Entry(T data, Entry<T> next) {
this.data = data;
this.next = next;
}
}
}
队列是一种先进先出,后进后出的线性表数据结构,其用途也十分广泛,比如二叉树层序遍历非递归实现、树或者图的广度优先遍历寻找最优路径信息,都需要用到队列这种数据结构。下面总结一下Java实现的循环队列和链式队列代码。
/**
* 循环队列
*/
class Queue<E>{
// 存储队列元素的数组
private E[] que;
// 表示队头的位置
private int front;
// 表示队尾的位置
private int rear;
/**
* 默认构造队列,初始大小是10
*/
public Queue(){
this(10);
}
/**
* 用户可以指定队列的大小size
* @param size
*/
public Queue(int size){
this.que = (E[])new Object[size];
this.front = this.rear = 0;
}
/**
* 入队操作
* @param val
*/
public void offer(E val){
if(full()){
// 扩容
E[] newQue = Arrays.copyOf(this.que,
this.que.length*2);
int index = 0;
for(int i=this.front;
i != this.rear;
i=(i+1)%this.que.length){
newQue[index++] = this.que[i];
}
this.front = 0;
this.rear = index;
this.que = newQue;
}
this.que[this.rear] = val;
this.rear = (this.rear+1)%this.que.length;
}
/**
* 出队操作,并把出队的元素的值返回
*/
public E poll(){
if(empty()){
return null;
}
E front = this.que[this.front];
this.que[this.front] = null;
this.front = (this.front+1)%this.que.length;
return front;
}
/**
* 查看队头元素
* @return
*/
public E peek(){
if(empty()){
return null;
}
return this.que[this.front];
}
/**
* 判断队满
* @return
*/
public boolean full(){
return (this.rear+1)%this.que.length == this.front;
}
/**
* 判断队空
* @return
*/
public boolean empty(){
return this.rear == this.front;
}
}
/**
* 链式队列
* front:指向的是链表的头节点
* rear: 永远指向的是末尾节点
* @param
*/
public class LinkQueue<T>{
// 指向头节点(队头)
private Entry<T> front;
// 指向尾节点(队尾)
private Entry<T> rear;
// 记录队列节点的个数
private int count;
/**
* 初始化,front和rear都指向头节点
*/
public LinkQueue(){
this.front = this.rear = new Entry<>(null, null);
}
/**
* 入队操作
* @param val
*/
public void offer(T val){
Entry<T> node = new Entry<>(val, null);
this.rear.next = node;
this.rear = node;
this.count++;
}
/**
* 出队操作
* @return
*/
public T poll(){
T val = null;
if(this.front.next != null){
val = this.front.next.data;
this.front.next = this.front.next.next;
// 删除队列最后一个元素,要把rear指向front,队列才能判空
if(this.front.next == null){
this.rear = this.front;
}
this.count--;
}
return val;
}
public T peek(){
T val = null;
if(this.front.next != null){
val = this.front.next.data;
}
return val;
}
/**
* 判断队列空
* @return
*/
public boolean isEmpty(){
return this.front == this.rear;
}
/**
* 返回队列元素的个数
* @return
*/
public int size(){
return this.count;
}
/**
* 节点类型定义
* @param
*/
static class Entry<T>{
T data;
Entry<T> next;
public Entry(T data, Entry<T> next) {
this.data = data;
this.next = next;
}
}
}