动手实现数据结构-数组、链表、队列、散列表

最近各种笔试、面试被虐,归结起来,打铁还需自身硬,想揽瓷器活,就得有金刚钻。任何各种投机取巧、侥幸心理都是没意义的。出来混,欠的总是要还的!在学习上,必须死磕到底。

数组

  • 数组(array),是有限个相同类型的变量所组成的有序集合,每一个变量被称为元素。
  • 数组是最为简单、最为常用的数据结构。
  • 在内存中顺序存储,可以很好地实现逻辑上的顺序表
  • 根据下标读取元素的方式叫作随机读取
  • 数组适合的场景:读操作多、写操作少。
  • 数据结构的操作无非是增、删、改、查4中情况。

实现数组扩容、插入、删除、输出的功能。

public class MyArray {
    //表示定义了一个引用变量(也就是定义了指针),未指向任何有效内存。
	private int[] array;
	private int size;
	public MyArray(int capacity) {
		this.array = new int[capacity];
		size = 0;
	}
	/**
	 * 数组插入元素
	 * @param element 插入的元素
	 * @param index 插入的位置
	 * @throws Exception
	 */
	public void insert(int element,int index) throws Exception {
		//判断访问下标是否超出范围
		if(index < 0 || index > size) {
			throw new IndexOutOfBoundsException("超出数组实际元素范围!!");
		}
		//
		if(size >= array.length) {
			resize();
		}
		//从右向左循环,将元素逐个向右挪1位
		for(int i=size-1;i>=index;i--) {
			array[i+1] = array[i];
		}
		//腾出的位置放入新元素
		array[index] = element;
		size++;
	}
	
	/**
	 * 数组删除元素
	 * @param index 删除元素的位置
	 * @return
	 */
	public int delete(int index) throws Exception {
		//判断访问下标是否超出范围
		if(index > size || index <= 0) {
			throw new IndexOutOfBoundsException("超出数组实际元素范围!");
		}
		//将要删除的元素缓存,在下一步返回
		int deleteElement = array[index];
		//从左向右循环,将元素逐个向左挪1位
		for(int i=index;i<size-1;i++) {
			array[i] = array[i+1];
		}
		//长度变化
		size--;
		return deleteElement;
	}
	
	/**
	 * 数组扩容
	 */
	private void resize() {
		int[] arrayNew = new int[array.length*2];
		//从旧数组复制到新数组
		System.arraycopy(array,0,arrayNew,0,array.length);
		array = arrayNew;
		
	}
	/**
	 * 输出数组
	 */
	public void output() {
		for(int i=0;i<size;i++) {
			System.out.println(array[i]);
		}
	}
	
	public static void main(String[] args) throws Exception {
		MyArray myArray = new MyArray(4);
		myArray.insert(3,0);
		myArray.insert(7,1);
		myArray.insert(9,2);
		myArray.insert(5,3);
		myArray.insert(6,1);
		myArray.output();
		System.out.println(myArray.size);
		
	}

}

链表

  • 链表(linked list)是一种在物理上非连续、非顺序的数据结构,由若干节点(node)组成。
  • 链表在内存中的存储方式是随机存储
  • 单向链表的每一个节点又包括两部分,一部分是存放数据的变量data,另一部分是指向下一个节点的指针next。
private static class Node{
	int data;
	Node next;
}

实现:

主要思想是将数据用node包装,然后node这种形式更加健壮和全面,不仅包含有data数据,还有指向下一个数据的指针,一个接一个,最后构成链表这种数据结构,实现链表这种功能。

每次添加的虽然是数据,一个int数据,但如果仅仅添加int数据的话,肯定是不行的,无法实现链表的特性。所以,每添加一个数据,都必须new一个新节点,通过节点对象的data变量保存数据,然后指向下一个node的指针next来确定node的逻辑关系。

个人的理解是:链表是一种设计和思想,但最终的实现是根据不同语言的特性来实现的,大同小异。

public class MyLinkedList {
	//头结点指针
	private Node head;
	//尾结点指针
	private Node last;
	//链表实际长度
	private int size;
	
	/**
	 * 链表插入元素
	 * @param data 插入元素
	 * @param index 插入位置
	 * @throws Exception
	 */
	public void insert(int data,int index) throws Exception{
		if(index < 0 || index > size) {
			throw new IndexOutOfBoundsException("超出链表节点范围!");
		}
		//要保证以链表的形式进行,虽然主要插入和操作的是数据data,但为了链表这种数据结构的方便性,还要套一层Node的壳,所以所有的数据都是以Node的形式存储
		//当插入数据的时候,必须先创建新的对象,将数据放到对象中去。
		Node insertedNode = new Node(data);
		if(size == 0) {
			//空链表
			head = insertedNode;
			last = insertedNode;
		}else if(index == 0) {
			//插入头部
			insertedNode.next = head;
			head = insertedNode;
		}else if(index == size) {
			//插入尾部
			last.next = insertedNode;
			last = insertedNode;
		}else {
			//插入中间
			Node prevNode = get(index-1);
			insertedNode.next = prevNode.next;
			prevNode.next = insertedNode;
		}
		size++;
	}
	
	/**
	 * 链表删除元素
	 * @param index 删除的位置
	 * @return
	 * @throws Exception
	 */
	public Node remove(int index) throws Exception{
		if(index<0 || index>size) {
			throw new IndexOutOfBoundsException("超出链表节点范围!");
		}
		//删除的节点,需要将此节点返回,先将其初始化为null,确定最后要删除那个节点,将此节点赋值给removeNode,作为函数的返回值
		Node removeNode = null;
		if(index == 0) {
			//删除头节点
			removeNode = head;
			head = head.next;
		}else if(index == size) {
			//删除尾结点
			Node prevNode = get(index-1);
			removeNode = prevNode.next;
			prevNode.next = null;
			last = prevNode;
		}else {
			//删除中间节点
			Node prevNode = get(index-1);
			Node nextNode = prevNode.next.next;
			removeNode = prevNode.next;
			prevNode.next = nextNode;
		}
		size--;
		return removeNode;
		
	}
	
	/**
	 * 链表查找元素
	 * @param index 查找的位置
	 * @return
	 * @throws Exception
	 */
	private Node get(int index) throws Exception {
		if(index<0 || index>size) {
			throw new IndexOutOfBoundsException("超出链表节点范围!");
		}
		Node temp = head;
		for(int i=0;i<index;i++) {
			temp = temp.next;
		}
		return temp;
	}
	
	/**
	 * 输出链表
	 */
	public void output() {
		Node temp = head;
		while(temp != null) {
			System.out.println(temp.data);
			temp = temp.next;
		}
	}

	/*
	 * 链表节点
	 */
	private static class Node{
		int data;
		Node next;
		Node(int data){
			this.data = data;
		}
	}
	
	public static void main(String[] args) throws Exception {
		MyLinkedList myLinkedList = new MyLinkedList();
		myLinkedList.insert(3, 0);
		myLinkedList.insert(7, 1);
		myLinkedList.insert(9, 2);
		myLinkedList.insert(5, 3);
		myLinkedList.insert(6, 1);
		myLinkedList.remove(0);
		myLinkedList.output();
		
	}

}

物理结构和逻辑结构

大多数数据结构都是以数组或链表作为存储方式。数组和链表可以被看作数据存储的“物理结构”。

 线性结构非线性结构
逻辑结构顺序表、栈、队列树、图
物理结构数组链表

栈和队列

两者都属于逻辑结构,它们的物理实现既可以利用数组,也可以利用链表来完成。

  • 栈(stack)是一种线性数据结构。
  • 栈中元素先入后出(First In Last Out,简称FILO)。
  • 最早进入的元素存放的位置叫做栈底(bottom),最后进入的元素存放的位置叫做栈顶(top)。

队列

  • 队列(queue)是一种线性数据结构。
  • 队列中的元素先入先出(First In First Out,简称FIFO)。
  • 队列的出口端叫作队头(front),队列的入口端叫作队尾(rear)。
public class MyQueue {
	private int[] array;
	private int front;
	private int rear;
	
	public MyQueue(int capacity) {
		this.array = new int[capacity];
	}
	
	/**
	 * 入队
	 * @param element 入队的元素
	 * @throws Exception
	 */
	public void enQueue(int element) throws Exception{
		if((rear+1)%array.length == front) {
			throw new Exception("队列已满!");
		}
		array[rear] = element;
		rear = (rear+1)%array.length;
	}
	
	/**
	 * 出队
	 * @return
	 * @throws Exception
	 */
	public int deQueue() throws Exception{
		if(rear == front) {
			throw new Exception("队列已空!");
		}
		int deQueueElement = array[front];
		front = (front+1)%array.length;
		return deQueueElement;
	}
	
	/**
	 * 输出队列
	 */
	public void output() {
		for(int i=front;i!=rear;i=(i+1)%array.length) {
			System.out.println(array[i]);
		}
	}
	
	public static void main(String[] args) throws Exception {
		MyQueue myQueue = new MyQueue(6);
		myQueue.enQueue(3);
		myQueue.enQueue(5);
		myQueue.enQueue(6);
		myQueue.enQueue(8);
		myQueue.enQueue(1);
		myQueue.deQueue();
		myQueue.deQueue();
		myQueue.deQueue();
		myQueue.enQueue(2);
		myQueue.enQueue(4);
		myQueue.enQueue(9);
		myQueue.output();
	}
	
}

散列表

  • 散列表也叫哈希表(hash table),这种数据结构提供了键(Key)值(Value)的映射关系。
  • 散列表本质上也是数组。
  • 散列表的Key是以字符串类型为主的。
  • 哈希函数,把Key和数组下标进行转换。
  • 每个对象都有属于自己的hashcode,这个hashcode是区分不同对象的重要标识。无论对象自身的类型是什么,它们的hashcode都是一个整型变量。
  • 将整型变量转化为数组的下标,最简单的方法就是按照数组长度进行取模运算。
  • 通过哈希函数,可以吧字符串或其他类型的Key,转化为数组的下标index。

写操作(put)

  • 就是在散列表中插入新的键值对(JDK中叫作Entry)。
  • 操作步骤,如调用hashMap.put("002931","老王"),意思是插入一组Key为002931、Value为老王的键值对:
  • 第一步:通过哈希函数,将Key转化为数值5;
  • 第二步:如果数组下标5对应的位置没有元素,就把这个Entry填充到数组下标为5的位置。

但是由于数组的长度是有限的,当插入的Entry越来来越多时,不同的Key通过哈希函数获得的下标都可能是相同的。这种情况,叫作哈希冲突。解决哈希冲突的方法主要有两种:

  1. 开放寻址法:ThreadLocal采用
  2. 链表法:HashMap应用

读操作(get)

  • 通过给定的Key,在散列表中查找到对应的Value。
  • 例如调用hashMap.get("002936"),意思是查找Key为002936的Entry在散列表中所对应的值。
  • 具体步骤:
  • 第一步:通过哈希函数,把Key转化为数组下标2;
  • 第二步:找到数组下标2所对应的元素,如果这个元素的Key是002936,那么就找到了;如果这个Key不是002936也没关系,由于数组的每个元素都与一个链表对应,我们可以顺着链表慢慢向下找,看看能否找到与Key相匹配的节点。

 

扩容(resize)

对于JDK中散列表实现类HashMap来说,影响其扩容的因素有两个。

  • Capacity,即HashMap的当前长度
  • LoadFactor,即HashMap的负载因子,默认值为0.75f
  • 衡量HashMap需要进行扩容的条件:HashMap.Size >= Capacity × LoadFactor

散列表的扩容经过两个步骤:

  1. 扩容,创建一个新的Entry空数组,长度是原数组的2倍。
  2. 重新Hash,遍历原Entry数组,把所有的Entry重新Hash到新数组中。

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值