前言
最近基本看完了《算法》这本巨作,这本书所涵盖的信息量超乎我的想象,原来不太理解的java.utils下的封装慢慢的揭开了面纱,而你也不得不敬佩这些封装java底层的人的算法水平,从简单的数组、队列到基础数据结构的排序,每一个函数的封装可能都是不断总结前人经验,并随着java版本的不断更迭而诞生和更新的。
就如一个简单的Array.sort(),当真正接触到它的代码实现时,我真的惊呆了,它不是一种排序的衍生物,而是由多种排序组合而成,由数组的长度来判断使用何种排序时间与空间开销较为合适。所以,底层的世界尤为繁杂,要学的东西还有很多…
算法可以说是整个底层基石的一大块,理解了各种复杂的数据结构与算法,才算是真正对计算机底层有了更深的理解。(另外一块则是操作系统,当然编译原理也算是,但是大多数人应该不会深究)
我需要研究的大概还有很多,但是为了防止学新而忘旧的现象,还是要好好停下来总结一下,也希望对看到这篇博客的其他人有所帮助。
正文
链表
简单分析
这种数据结构应该不必多言,这是大多数高级数据结构的基础,选择链表的大多数原因是为了弥补数组定长的缺陷。如果用它来进行如查找、排序等功能,其时间复杂度一定远大于数组(数组的访问时间复杂度为O(1),而链表则为O(n)),因此链表多用于进行动态扩展并且不需要进行排序的数据结构中,如队列,栈、背包、散列表等。
链表数据结构主要能够实现的功能如:
(1)插入一个节点
(2)删除指定值的节点
(3)遍历输出(需要实现Iterable接口)
当然,链表还涉及到是否有头结点这一说,对于大多数情况来说,有一个空白头结点可能会比没有头结点要好一些,因为有时候头指针first=null
会很让人头疼,当然,不使用似乎也没什么问题。
注:在实现过程中,运用泛型来定义存储值的类型非常灵活,这正像java.util中大多数数据结构一致,并且一般使用的泛型都是java的包装类,因为这些类基本实现了Comparable接口
基本实现
public class LinkedList<Item> implements Iterable<Item>{
private class Node<Item>{
public Item val;
public Node next;
public Node(){}
public Node(Item val){
this.val = val;
this.next = null;
}
}
public Node<Item> first;
public int N;
public LinkedList(){
first = new Node<>();
N=0;
}
//获取链表中第i个元素
public Item get(int i){
if(i>=N)return null;
int m = i;
Node<Item> target = first.next;
while(m>0)target=target.next;
return target;
}
//插入操作,头插法
public void insert(Item item){
Node<Item> current = new Node(item);
current.next =first.next;
first.next = current;
N++;
}
//删除操作
public void delete(Item item){
Node current = first;
while(current.next!=null){
if(current.next.val.equals(item)){
Node deleted = current.next;
current.next = current.next.next;
N--;
}
current = current.next;
}
}
//遍历输出节点内容
@Override
public Iterator<Item> iterator(){
return new NodeValueIterator<Item>(first);
}
//实现NodeValueIterator类
private class NodeValueIterator<Item> implements Iterator<Item>{
private Node<Item> current;
public NodeValueIterator(Node<Item> first){
this.current = first;
}
@Override
public boolean hasNext(){
return current.next!=null;
}
@Override
public Item next(){
if(!hasNext())
throw new NoSuchElementException();
current=current.next;
Item currentVal = current.val;
return currentVal;
}
}
}
注意是否有头结点可能会对代码的整体影响很大,如果未使用头结点,在插入或者删除前务必先判断first==null
,编代码时需要特别注意。
栈
简单分析
栈是在计算机底层使用非常频繁的一种数据结构,其作用大多用于保存函数参数或者保存寄存器状态,当然还有如计算公式时需要使用符号栈与数值栈,或者中序表达式转后序表达式这样的问题,一般也需要用到栈。其主要功能后进先出的特性让其有如此的用途。
栈的构造有两种方式,一个是使用数组构造,而另一个则是使用上面实现的队列构造。
当然一开始会有疑问:数组无法动态扩展,如何让其充当栈的基础存储结构?
这就要说到Java的GC机制——垃圾回收机制,当然很多人吐槽他,因为其引起的性能问题使得很多追求速度的程序员无法直视这种机制,甚至他们觉得构造一个析构要比GC要好得多。
但是GC确实也给我们带来了便利,我们只需将某个数组的所有指针/引用去除,就可以让系统自动回收我们开销的数组空间,而不是我们自己来删除。
实现
这里只实现一下数组为基础的栈,因为以链表为基础的栈其主要还是头插法和头部删除法,相对简单。
public class Stack<Item>{
private Item[] items;
public int N;
public Stack(){
items = (Item[]) new Object[5];
N=0;
}
//扩展或缩减数组函数
public void resize(int length){
Item[] a = (Item[]) new Object[length];
for(int i=0;i<N;i++)
a[i] = this.items[i];
this.items = a;
}
//查看栈是否为空
public boolean isEmpty(){
return N==0;
}
//将元素压入栈
public void push(Item item){
if(N==items.length)
resize(2*N);
items[N++] = item;
}
//将元素弹出栈
public Item pop(Item item){
if(N<=items.length/4)
resize(items.length/2);
if(isEmpty())
return null;
Item popItem = this.items[--N];
return popItem;
}
//查看最近压入的元素
public Item peek(){
return this.items[N-1];
}
}
队列
简单分析
队列是区别于栈的另一种数据结构,队列遵循着先进先出的基本原则,因此队列主要用于实现类似缓存功能的相关内容,如操作系统最核心的部分就是对于进程优先队列的把控和使用,当然还有循环队列、双向队列等一些队列的变种,用于各种实际应用中。
队列的基础数据结构可以为数组,可以为链表,甚至像优先队列这样需要内部排序的队列可以使用二叉堆来做基础数据结构,堆的构造后面再详细讲解。
实现
这里我们使用链表来构造一个普通队列,其中需要两个关键引用,一个指向队头,一个指向队尾,通过改变这两个指针来控制队列的增长与减小。(当然如果提前实现了双向链表则可能不需要这样麻烦)
public class Queue<Item>{
private Node<Item> first;
private Node<Item> last;
private int N;
public Queue(){
first=new Node();
last = first;
N=0;
}
public int Length(){return N;}
//判断队列是否为空
public boolean isEmpty(){
return N==0;
}
//加入队列操作,向队尾插入
public void enqueue(Item value){
Node<Item> inNode = new Node(val);
last.next = inNode;
last=inNode;
N++;
}
//出队列,从队头删除
public Item dequeue(){
if(isEmpty())return null;
Node<Item> deleted = first.next;
first.next = first.next.next;
N--;
//如果删除完后发现为空,则last指向头结点
if(isEmpty())last=first;
return deleted.val;
}
}
这个地方还是要注意有无头结点的区别。
背包
简单分析
这种数据结构相对队列和栈来说可能不会太熟悉,因为有时候队列确实就可以替代他的功能。一个背包数据结构只能向里面填充,不能从里面取出,即只能添加,不能删除。这种数据结构看似不可理解,但是实际中,当你只想要添加而不必删除修改内容时,这样的数据结构可以防止你的误操作。
就如已知一个图结构,这个图可能未来不需要删减元素,而只是一味的扩充的话,使用Bag数据结构来存储某个节点的邻接关系再合适不过。
当然实现Bag也可以是两种方式:链表和数组,这里我们主要实现一下链表为基础的Bag。
实现
实现一个Bag显然要比栈或者队列要简单,因为他不用考虑有关删除的操作。(这里展示一下没有头结点的链表)
public class Bag<Item> implements Iterable<Item>{
private Node first;
private int N;
public Bag(){
first=null;
N=0;
}
//向背包中添加物品
public void add(Item item){
Node current = new Node(item);
if(first==null){first =current;return ;}
current.next = first.next;
first.next = current;
}
//获取背包中第i个元素
public Item get(int i){
int m = i;
Node<Item> target = first;
while(m>0&&target!=null){target=target.next;m--;}
if(m>0)return null;
return target.val;
}
@Override
public Iterator<Item> iterator(){
return new BagIterator<Item>(first);
}
//BagIterator内容与上面编写的NodeValueIterator基本一致,只是注意有无头结点的不同实现
private class BagIterator<Item> implements Iterator<Item>{
...
}
}
总结
基础数据结构基本就是数组(无需我们构造)、链表、队列、栈,其实这些java.util包当中都已经为用户编写好了相应的数据结构,但是对于学习算法来说,这些基础数据结构又相当关键。不积跬步无以至千里的道理都明白,只有掌握这些基础数据结构,才能很好的处理面试题当中如循环队列、双向队列(栈)、优先队列、随机背包这样的问题,也正因为有了这些数据结构,图、树、散列表等高级数据结构构造才能变得简单。
下一篇应该会先总结排序,因为其确实相当重要。