最近各种笔试、面试被虐,归结起来,打铁还需自身硬,想揽瓷器活,就得有金刚钻。任何各种投机取巧、侥幸心理都是没意义的。出来混,欠的总是要还的!在学习上,必须死磕到底。
数组
- 数组(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通过哈希函数获得的下标都可能是相同的。这种情况,叫作哈希冲突。解决哈希冲突的方法主要有两种:
- 开放寻址法:ThreadLocal采用
- 链表法: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
散列表的扩容经过两个步骤:
- 扩容,创建一个新的Entry空数组,长度是原数组的2倍。
- 重新Hash,遍历原Entry数组,把所有的Entry重新Hash到新数组中。