数据结构学习,栈和队列以及链表(java语言)
1.栈(Stack)
栈是一种后进先出(Last In First Out)的数据结构,只能从同一端放入数据和取出数据
具体应用有:
- 文档编辑器的Undo(撤销)操作:每次对文档进行操作,就将操作压栈,撤销时就将栈顶操作弹出,就完成了撤销操作
- 代码运行时的方法调用:当一个方法的执行需要调用另一个方法时,调用前会将本次方法以及执行的位置入栈,执行被调用的方法结束后,在将栈顶的方法弹出,继续执行
首先定义了一个栈的接口,声明了栈所需要的操作
public interface Stack<E> {
int getSize();//获取当前栈的大小
boolean isEmpty();//判断栈是否为空
void push(E e);//元素入栈操作
E pop();//元素出栈操作
E peek();//取出栈顶元素
}
1.1数组栈(ArrayStack)
数组栈的底层是通过昨天学习的动态数组实现的,实现比较容易,在新建栈对象的时候新建一个数组对象,并作为栈的成员变量,之后对栈进行的操作都是对这个数组进行的操作,本质是调用一下这个数组已定义的一些方法(入栈addLast()、出栈removeLast()、取顶getLast())。判断是栈的大小和是否为空栈就可以调用数组中的getSize()和isEmpty()方法。
在这些方法中,由于都是对栈顶,即数组的尾部进行操作,所以均摊复杂度为O(1)
public class ArrayStack<E> implements Stack<E> {
private Array<E> array;
public ArrayStack(int capacity){
array = new Array<>(capacity);
}
public ArrayStack(){
array = new Array<>();
}
@Override
public int getSize(){
return array.getSize();
}
@Override
public boolean isEmpty(){
return array.isEmpty();
}
public int getCapacity(){
return array.getCapacity();
}
@Override
public void push(E e){
array.addLast(e);
}
@Override
public E pop(){
return array.removeLast();
}
@Override
public E peek(){
return array.getLast();
}
@Override
public String toString(){
StringBuilder res = new StringBuilder();
res.append("Stack:");
res.append("[");
for(int i = 0 ; i < array.getSize() ; i++){
res.append(array.get(i));
if(i != array.getSize() - 1 ){
res.append(",");
}
}
res.append("]top");
return res.toString();
}
}
1.2链表栈(LinkedListStack)
链表栈的底层是通过一个链表实现,由于链表本身的结构,对链表头进行操作的复杂度都为O(1),刚好栈只需要对一端进行增减操作,所以链表非常适合来作为栈的底层结构,通过链表中的头操作(addFirst(),removeFirst(),getFirst())即可轻松实现栈的结构
public class LinkedListStack<E> implements Stack<E> {
private LinkedList<E> list;
public LinkedListStack(){
list = new LinkedList<>();
}
@Override
public int getSize(){
return list.getSize();
}
@Override
public boolean isEmpty(){
return list.isEmpty();
}
@Override
public void push(E e){
list.addFirst(e);
}
@Override
public E pop(){
return list.removeFirst();
}
@Override
public E peek(){
return list.getFirst();
}
@Override
public String toString(){
StringBuilder res = new StringBuilder();
res.append("Stack : top ");
res.append(list);
return res.toString();
}
public static void main(String[] args) {
LinkedListStack<Integer> stack = new LinkedListStack<>();
for (int i = 0; i < 5; i++) {
stack.push(i);
System.out.println(stack);
}
stack.pop();
System.out.println(stack);
}
}
2.队列(Queue)
队列是一种先进先出(First In Fist Out)的数据结构,平时生活中的排队基本上都是可以看成是一种队列的结构,例如去银行取钱,大家从依次排好顺序,先来的人站前,后来的人只能站后面,先来的人先办完事,就可以先走了,这就是一种队列的结构,同样的,对队列的操作我也定义了一个接口
public interface Queue<E> {
void enqueue(E e);//入队
E dequeue();//出队
E getFront();//获取队首元素
int getSize();//获取队列长度
boolean isEmpty();//判断队列是否为空
}
2.1数组队列(ArrayQueue)
数组队列与数组栈基本相同,都是底层通过一个动态数组实现,只不过数组队列是从数组的一头进,另一头出,而数组栈都是同一头进出。
public class ArrayQueue<E> implements Queue<E> {
private Array<E> array;
public ArrayQueue(int capacity){
array = new Array<>(capacity);
}
public ArrayQueue(){
array = new Array<>();
}
@Override
public void enqueue(E e){
array.addLast(e);
}
@Override
public E dequeue(){
return array.removeFirst();
}
@Override
public E getFront(){
return array.getFirst();
}
@Override
public int getSize(){
return array.getSize();
}
@Override
public boolean isEmpty(){
return array.isEmpty();
}
public int getCapacity(){
return array.getCapacity();
}
@Override
public String toString(){
StringBuilder res = new StringBuilder();
res.append("Queue: ");
res.append("front [");
for(int i = 0 ; i < array.getSize() ; i++){
res.append(array.get(i));
if(i != array.getSize() - 1){
res.append(",");
}
}
res.append("] tail");
return res.toString();
}
}
看了代码,不难发现,入队的操作时间复杂度是O(1),但是出队操作由于要从数组头取出一个元素,就要将数组中后面的每一个元素都向前移动一位,这样做的时间复杂度是O(n),那有没有什么办法能解决这一问题呢?所以我们对这个底层的数组进行了改进,加上一个记录队首索引的成员变量和记录队尾索引的成员变量,将数组进行改进,成为一个循环数组
2.2循环队列(LoopQueue)
具体操作其实不难,这时的底层就是我们要新修改的数组,不用原来的数组
private E[] data;
private int front;
private int tail;
private int size;
定义了以上变量,front用来记录队首索引,tail用来记录队尾索引,当front = tail时,队列为空,当tail + 1 = front 时,队列为满,也就是说,队列满的时候,我们其实有一个空间是浪费的,即tail索引指向的空间是浪费的,所以在声明的容量的时候,我们要将队列实际的容量+1才能得到这么大的容量。
有了队首和队尾之后,我们对队列的操作就简单了许多,当入队时,我们只要将元素插入tail所指向的索引处,然后维护一下tail 将tail+1即可(实际上tail+1不准确,由于front会进行移动,所以队列可能会出现数组前几个索引是空的情况,这时要将tail重新指向这些地方,所以入队后tail应该是等于(tail+1)%data.length)。出队操作也十分容易,将front索引处的元素释放,然后将front+1(实际上与tail相同,都要(front+1)%data.length),就完成了一次出队操作。
这样一来,入队和出队都只需要对front、tail的索引处进行操作,有时会涉及到扩容的操作,所以均摊复杂度为O(1),就改善了普通数组队列出队时的时间复杂度是O(n)的情况。
public class LoopQueue<E> implements Queue<E> {
private E[] data;
private int front;
private int tail;
private int size;
public LoopQueue(int capacity){
data = (E[])new Object[capacity + 1];
front = 0;
tail = 0;
size = 0;
}
public LoopQueue(){
this(10);
}
public int getCapacity(){
return data.length - 1;
}
@Override
public boolean isEmpty(){
return front==tail;
}
@Override
public int getSize(){
return size;
}
@Override
public void enqueue(E e){
if((tail + 1) % data.length == front){
resize(getCapacity() * 2);
}
data[tail] = e;
tail = (tail + 1) % data.length;
size ++;
}
@Override
public E dequeue(){
if(isEmpty()){
throw new IllegalArgumentException("Cannot dequeue from an empty queue");
}
E ret = data[front];
data[front] = null;
front = (front + 1) % data.length;
size--;
if(size == getCapacity() /4 && getCapacity()/2 != 0){
resize(getCapacity()/2);
}
return ret;
}
@Override
public E getFront(){
if(isEmpty()){
throw new IllegalArgumentException("Cannot getFront from an empty queue");
}
return data[front];
}
private void resize(int newCapacity){
E[] newData = (E[])new Object[newCapacity+1];
for(int i = 0 ; i < size ; i ++ ){
newData[i] = data[(front+i) % data.length];
}
data = newData;
front = 0 ;
tail = size;
}
@Override
public String toString(){
StringBuilder res = new StringBuilder();
res.append(String.format("Queue : size = %d , capacity = %d \n",size,getCapacity()));
res.append("front [");
for(int i = front ; i != tail ; i=(i+1) % data.length){
res.append(data[i]);
if((i+1) % data.length != tail){
res.append(",");
}
}
res.append("] tail");
return res.toString();
}
public static void main(String[] args) {
LoopQueue<Integer> arr = new LoopQueue<>();
for(int i = 0 ; i<100000 ; i++){
arr.enqueue(i);
System.out.println(arr);
if(i % 3 == 2){
arr.dequeue();
System.out.println(arr);
}
}
}
}
2.3链表队列(LinkedListQueue)
由于链表对头操作比较容易,对尾操作比较复杂,所以普通的链表做队列的话入队或出队会耗费一定的时间,所以我们同数组一样,在链表中设置一个尾节点,指向当前链表的尾部。当队列为空的时候,head = tail = null。由于链表是一种真正的动态结构(数组的扩容其实也是静态的,底层是我们不断新建大容量数组的替换),不会浪费空间。
public class LinkedListQueue<E> implements Queue<E> {
private class Node{
public E e;
public Node next;
public Node(E e,Node next){
this.e = e;
this.next = next;
}
public Node(E e){
this(e,null);
}
public Node(){
this(null,null);
}
@Override
public String toString(){
return e.toString();
}
}
private Node head,tail;
private int size;
public LinkedListQueue(){
head = null;
tail = null;
size = 0;
}
@Override
public int getSize(){
return size;
}
@Override
public boolean isEmpty(){
return size == 0 ;
}
@Override
public void enqueue(E e){
if(tail == null){
Node node = new Node(e);
tail = node;
head = tail;
}else{
tail.next = new Node(e);
tail = tail.next;
}
size++;
}
@Override
public E dequeue(){
if(isEmpty()){
throw new IllegalArgumentException("Cannot dequeue from an empty queue");
}
Node retNode = head ;
head = head.next;
retNode.next = null;
if(head == null){
tail = null;
}
size -- ;
return retNode.e;
}
@Override
public E getFront(){
if(isEmpty()){
throw new IllegalArgumentException("Queue is empty");
}
return head.e;
}
@Override
public String toString(){
StringBuilder res = new StringBuilder();
res.append("Queue : front ");
Node cur = head;
while(cur!=null){
res.append(cur + "->");
cur = cur.next;
}
res.append("NULL tail");
return res.toString();
}
public static void main(String[] args) {
LinkedListQueue<Integer> arr = new LinkedListQueue<>();
for(int i = 0 ; i<10 ; i++){
arr.enqueue(i);
System.out.println(arr);
if(i % 3 == 2){
arr.dequeue();
System.out.println(arr);
}
}
}
}
3.链表
链表是一种真正的,最基础的动态数据结构,基础的链表头节点指向索引为0的地方,但是链表有一个特性,就是对当前节点操作麻烦,对当前节点的下一节点,即next节点操作简单,所以我们定义了一个虚拟头节点dummyHead,里面啥也不存,就用来指向真正链表的头节点。
3.1链表的基础结构
首先,我们在链表类中定义了一个内部类,用来存放节点的结构
private class Node{
public E e;
public Node next;
public Node(E e,Node next){
this.e = e;
this.next = next;
}
public Node(E e){
this(e,null);
}
public Node(){
this(null,null);
}
@Override
public String toString(){
return e.toString();
}
}
然后为链表类添加两个成员变量,分别是dummyHead和size,用来记录虚拟头节点的位置和当前链表的长度
private Node dummyHead;
private int size;
而后就是定义一些关于链表的基本操作
3.2链表的初始化
链表的初始化就是生成一个链表的虚拟头节点以及设置一下链表的size=0 就完成了链表的初始化
public LinkedList(){
dummyHead = new Node(null,null);
size = 0;
}
3.2链表的插入与删除操作
在实际工程中,对链表的插入与删除基本上只要对链表的头进行操作,没有索引的概念,索引的概念只是在起一个练习的作用
新增操作(add()):
public void add(int index,E e){
if(index < 0 || index > size){
throw new IllegalArgumentException("add failed.Illegal index.");
}
Node prev = dummyHead;
for(int i = 0 ; i<index ; i ++){
prev = prev.next;
}
// Node node = new Node(e);
// node.next = prev.next;
// prev.next = node;
prev.next = new Node(e,prev.next);
size ++;
}
首先检查一下索引的正确性,然后将前驱节点prev从虚拟头节点出发,不断下移,直到指向index处的前一个节点。最后将新生成节点插入前驱节点的下一个节点并连接上后面的节点就完成了插入操作。当然最后还得维护一下size变量,将他进行加一操作。
删除操作(remove()):
public E remove(int index){
if(index < 0 || index >= size){
throw new IllegalArgumentException("set failed.Illegal index.");
}
Node prev = dummyHead;
for(int i = 0 ; i < index; i++){
prev = prev.next;
}
Node delNode = prev.next;
prev.next = delNode.next;
delNode.next = null;
size -- ;
return delNode.e;
}
与新增操作大同小异,都是通过前驱节点找到所要删除的节点,然后继续操作。
链表的总代码如下
public class LinkedList<E> {
private class Node{
public E e;
public Node next;
public Node(E e,Node next){
this.e = e;
this.next = next;
}
public Node(E e){
this(e,null);
}
public Node(){
this(null,null);
}
@Override
public String toString(){
return e.toString();
}
}
private Node dummyHead;
private int size;
public LinkedList(){
dummyHead = new Node(null,null);
size = 0;
}
public int getSize(){
return size;
}
public boolean isEmpty(){
return size==0;
}
public void addFirst(E e){
// Node node = new Node(e);
// node.next = head;
// head = node;
// dummyHead.next = new Node(e, dummyHead.next);
// size ++;
add(0,e);
}
public void add(int index,E e){
if(index < 0 || index > size){
throw new IllegalArgumentException("add failed.Illegal index.");
}
Node prev = dummyHead;
for(int i = 0 ; i<index ; i ++){
prev = prev.next;
}
// Node node = new Node(e);
// node.next = prev.next;
// prev.next = node;
prev.next = new Node(e,prev.next);
size ++;
}
public void addLast(E e){
add(size,e);
}
public E get(int index){
if(index < 0 || index >= size){
throw new IllegalArgumentException("get failed.Illegal index.");
}
Node cur = dummyHead.next;
for(int i = 0 ;i < index ; i ++){
cur=cur.next;
}
return cur.e;
}
public E getFirst(){
return get(0);
}
public E getLast(){
return get(size-1);
}
public void set(int index , E e){
if(index < 0 || index >= size){
throw new IllegalArgumentException("set failed.Illegal index.");
}
Node cur = dummyHead.next;
for(int i = 0; i < index ; i++){
cur = cur.next;
}
cur.e = e ;
}
public boolean contains(E e){
Node cur = dummyHead.next;
while(cur != null){
if(cur.e.equals(e)){
return true;
}
cur = cur.next;
}
return false;
}
public E remove(int index){
if(index < 0 || index >= size){
throw new IllegalArgumentException("set failed.Illegal index.");
}
Node prev = dummyHead;
for(int i = 0 ; i < index; i++){
prev = prev.next;
}
Node delNode = prev.next;
prev.next = delNode.next;
delNode.next = null;
size -- ;
return delNode.e;
}
public E removeFirst(){
return remove(0);
}
public E removeLast(){
return remove(size-1);
}
@Override
public String toString(){
StringBuilder res = new StringBuilder();
Node cur = dummyHead.next;
while(cur!=null){
res.append(cur + "->");
cur = cur.next;
}
res.append("NULL");
return res.toString();
}
}
4.总结
- 栈是后进先出的数据结构,具体实验了两种实现方法,数组栈和链表栈。数组栈新增与删除操作都是对数组尾部进行操作,由于有size的存在,知道尾部在哪,不过要不断扩容,所以均摊复杂度为O(1)。链表栈由于都只对头节点进行操作,时间复杂度也是一样都为O(1),但是链表所实现的是真正动态。
- 队列是先进先出的数据结构,首先用了数组队列,发现出队时由于对队首进行操作需要进行后续元素的迁移,导致时间复杂度为O(n),为解决此方法,我们改进了队列成为循环队列,在数组中加入了一个front变量和tail变量,分别存储头尾的索引。这样入队和出队操作都变成了均摊复杂度为O(1)的操作了。最后我们也能用改进的链表实现链表队列这个结构,在链表中加入一个记录尾部的tail节点,就可以方便尾部数据插入,而头部数据插入和删除都是容易的,所以用链表实现的这个队列结构也是时间复杂度都为O(1)的。
- 链表是一种真正的动态的数据结构。对头部的新增删除查询操作容易,对搜索和其他位置的新增删除操作不容易。