算法——数据结构基础(数组、队列、栈、链表、散链表)
算法——数据结构基础
文章结构概览
数组:顺序存储
读操作多,写操作少
- 1.读取(查找)元素:读取对应的下标
高效的查找元素的算法:二分查找 - 2.更新元素:对元素进行重新赋值
- 3.插入元素:尾部插入、中间插入、超范围插入
扩容问题,时间复杂度:O(n)
插入并移动元素,时间复杂度:O(n) - 4.删除元素:1) 删除对应位置,后面的元素往前挪
2) 把最后一个元素复制到删除元素所在的位置,再删除最后一个元素
删除元素,时间复杂度:O(n)
package array; /**
* Copyright (C), 2019-2020
* author candy_chen
* date 2020/6/16 9:58
* version 1.0
* Description: 数组
*/
/**
*测试数组从中间插入元素
*/
public class test_array {
private int[] array;
private int size;
public test_array(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("超出数组实际范围");
}
//从右向左循环,将元素逐个向右挪1位
for (int i=size -1;i>=index;i--){
array[i+1] = array[i];
}
//腾出的位置放入新元素
array[index] = element;
size++;
}
/**
* 数组扩容
*/
public void resize(){
int[] arrayNew = new int[array.length*2];
//从旧数组复制到新数组
System.arraycopy(array,0,arrayNew,0,array.length);
array = arrayNew;
}
/**
* 数组删除元素
* @param index 删除的位置
* @return
*/
public int delete(int index){
if (index <0 || index >size){
throw new IndexOutOfBoundsException("超出数组实际范围");
}
int deletedElement = array[index];
//从左向右循环
for (int i=index;i<size-1;i++){
array[i] = array[i+1];
}
size--;
return deletedElement;
}
/**
* 输出数组
*/
public void output(){
for (int i=0;i<size;i++){
System.out.println(array[i]);
}
}
public static void main(String[] args) throws Exception {
test_array test_array = new test_array(10);
test_array.insert(3,0);
test_array.insert(7,1);
test_array.insert(9,2);
test_array.insert(5,3);
test_array.insert(6,1);
test_array.output();
}
}
链表:随机存储
单链表
- 1.查找节点:时间复杂度:O(n)
从头节点开始向后一个一个节点逐一查找 - 2.更新节点:旧数据替换成新数据即可,时间复杂度:O(1)
- 3.插入节点:尾部插入、中间插入、头部插入,时间复杂度:O(1)
尾部插入:把最后一个节点的next指针指向新插入的节点
头部插入:把新节点的next指针指向原先的头节点
把新节点变成链表的头节点
中间插入:插入位置的前置节点的next指针指向插入的新节点
将新节点的next指针指向前置节点的next指针原先所指向的节点 - 4.删除节点:尾部删除、头部删除、中间删除,时间复杂度:O(1)
尾部删除:直接将倒数第二个节点的next指针指向空即可
头部删除:链表的头节点设为原先头节点的next指针即可
中间删除:把删除及诶单的前置节点的next指针指向要删除元素的下一个节点
MyLinkedList.java
package array;/**
* Copyright (C), 2019-2020
* author candy_chen
* date 2020/6/18 21:36
* version 1.0
* Description: 单链表的增删改查
*/
/**
*
*/
public class MyLinkedList {
//头节点指针
private Node head;
//尾节点指针
private Node last;
//链表实际长度
private int size;
/**
* 链表插入元素
* @param data 插入元素
* @param index 插入位置
*/
public void insert(int data,int index) throws Exception {
if (index < 0 || index > size){
throw new IndexOutOfBoundsException("超出链表节点范围");
}
Node insertedNode = new Node(data);
if (size == 0){
head = insertedNode;
last = insertedNode;
}else if (index == 0) {
//插入头部 把新节点的next指针指向原先的头节点
// 把新节点变成链表的头节点
insertedNode.next = head;
head = insertedNode;
}else if (size == index){
//插入尾部 把最后一个节点的next指针指向新插入的节点
last.next = insertedNode;
last = insertedNode;
}else{
//插入中间 插入位置的前置节点的next指针指向插入的新节点
// 将新节点的next指针指向前置节点的next指针原先所指向的节点
Node prevNode = get(index -1);
Node nextNode = prevNode.next;
prevNode.next = insertedNode;
insertedNode.next = nextNode;
}
//插入元素后需要给链表长度加一
size++;
}
/**
* 链表删除元素
* @param index 删除的位置
* @return removeNode 返回删除的节点
*/
public Node remove(int index){
if (index < 0||index >= size){
throw new IndexOutOfBoundsException("超出链表节点范围!!");
}
Node removeNode = null;
if (index == 0){
//删除头节点 链表的头节点设为原先头节点的next指针即可
removeNode = head;
head = head.next;
}else if (index == size-1){
//删除尾节点 直接将倒数第二个节点的next指针指向空即可
Node prevNode = get(index -1);
removeNode = prevNode.next;
prevNode.next = null;
last = prevNode;
}else {
//删除中间节点 把删除及诶单的前置节点的next指针指向要删除元素的下一个节点
Node prevNode = get(index -1);
Node nextNode = prevNode.next.next;
removeNode = prevNode.next;
prevNode.next = nextNode;
}
size--;
return removeNode;
}
/**
* 查找链表元素
* @param index 查找的位置
* @return temp
*/
private Node get(int index) {
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;
public 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.output();
System.out.println("------------------");
myLinkedList.insert(6,1);
myLinkedList.output();
System.out.println("------------------");
myLinkedList.remove(0);
myLinkedList.output();
System.out.println("------------------");
}
}
如何判断链表有环?
栈:
先进后出,FILO
- 入栈 时间复杂度:O(1)
- 出栈 时间复杂度:O(1)
MyStack.java
package array;/**
* Copyright (C), 2019-2020
* author candy_chen
* date 2020/6/19 20:10
* version 1.0
* Description: 测试
*/
/**
*
*/
public interface MyStack<Item> extends Iterable<Item> {
MyStack<Item> push(Item item);
Item pop() throws Exception;
boolean isEmpty();
int size();
}
ArrayStack.java
package array;/**
* Copyright (C), 2019-2020
* author candy_chen
* date 2020/6/19 20:09
* version 1.0
* Description: 测试
*/
import java.util.Iterator;
/**
*
*/
public class ArrayStack<Item> implements MyStack<Item> {
//栈元素数组,只能通过转型来创建泛型数组
private Item[] a = (Item[]) new Object[1];
//元素数量
private int N = 0;
@Override
public MyStack<Item> push(Item item) {
check();
a[N++] = item;
return this;
}
private void check() {
if (N > a.length){
resize(2 * a.length);
}else if (N > 0 && N<= a.length /4){
resize(a.length /2);
}
}
private void resize(int size) {
Item[] tmp = (Item[]) new Object[size];
for (int i = 0; i < N; i++) {
tmp[i] = a[i];
}
a = tmp;
}
@Override
public Item pop() throws Exception {
if (isEmpty()) {
throw new Exception("stack is empty");
}
Item item = a[--N];
check();
// 避免对象游离
a[N] = null;
return item;
}
@Override
public boolean isEmpty() {
return N == 0;
}
@Override
public int size() {
return N;
}
@Override
public Iterator<Item> iterator() {
// 返回逆序遍历的迭代器
return new Iterator<Item>() {
private int i = N;
@Override
public boolean hasNext() {
return i > 0;
}
@Override
public Item next() {
return a[--i];
}
};
}
}
队列:
先进先出 FIFO
出口端叫队头,入口端叫队尾
- 循环队列:
队列满了的判定条件: (队尾下标 + 1) % 数组长度 = 队头下标
(rear + 1 )%array.length == front
队列最大容量比数组长度小1 - 双端队列:
结合了栈和队列的特点,既可以先进先出,也可以先入后出
从队头一端可以出队或入队,从队尾一端也可以出队或入队 - 优先队列:
谁的优先级高,谁先出队(二叉堆实现的)
MyQueue.java
package array;/**
* Copyright (C), 2019-2020
* author candy_chen
* date 2020/6/19 11:12
* version 1.0
* Description: 循环队列
*/
/**
*
*/
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
散列表实现类(HashMap),提供了键(Key)和值(Value)的映射关系,只要给出一个Key就可以高效的找出匹配的Value,时间复杂度:O(1)
基本原理:
- 本质上是一个数组
1. 哈希函数
通过某种方式,把Key和数组下标进行转换,这个转换的中转站就叫做哈希函数
-
hashcode
-
HashMap
通过哈希函数,我们可以把字符串或者其他类型的key转换成数组的下标index,想要转化成数组的下标,做简单的转换方式是按照数组长度当进行取模运算
index = HashCode(key) % Array.length
例:给出一个长度为8的数组,当
key = 5720153303时,
index = HashCode(“5720153303”) % Array.length = 1420036703 % 8 = 7
key = this时,
index = HashCode(“this”) % Array.length = 3559070 % 8 = 6
2.散列表的读写操作:
(1) 写操作:put
就是在散列表中插入新的键值对(Entry) hashMap.put(“2020619”,“张三”)
哈希冲突:
数组的长度有限,当插入的Entry越来越多时,不同的Key通过哈希函数获得的下标有可能是相同的,则产生哈希冲突
解决方法:
-
- 开放寻址法
原理:当一个Key通过哈希函数获得对应的数组下标已经被占用时,则寻找下一个空档位置
应用:在java中,ThreadLocal使用的就是开放寻址法
- 开放寻址法
-
- 链表法
原理:HashMap数组的每一个元素不仅是一个Entry对象,还是一个链表的头节点,每一个Entry对象通过next指针指向它的下一个Entry节点
当新的Entry映射与之冲突时,则与该位置为头节点,插入到该链表中
应用:java中的HashMap
- 链表法
(2) 读操作:get
通过给定的Key,在散列表中查找对应的Value
步骤:
-
- 通过哈希函数,把Key转换成数组下标
-
- 如果不冲突,则返回对应的Value; 如果冲突,则顺着链表往下找,查找匹配的节点
(3) 扩容:resize
-
当大量元素拥挤在相同数组下标位置,形成很长的链表,对后续插入和查询会有很大影响,则需要对散列表进行扩容
散列表实现类(HashMap)而言,扩容因素: -
- Capacity,即HashMap的当前长度
-
- LoadFactor,即HashMap的负载因子,默认为0.75f
扩容条件:HashMap.Size >= Capacity x LoadFactor
扩容步骤:
- LoadFactor,即HashMap的负载因子,默认为0.75f
-
- 扩容:创建一个新的Entry数组,长度为原来的2倍
-
- 重新Hash:遍历元Entry数组,把原来的Entry重新Hash到新数组中。
重新Hash是因为长度扩大,Hash的规则也随之改变。原本拥挤的散列表重新变得稀疏,原来的Entry也红心得到尽可能的均匀分配
- 重新Hash:遍历元Entry数组,把原来的Entry重新Hash到新数组中。
说明:根据网络资料进行搜索学习理解整理 若有侵权联系作者
- 参考书籍:漫画算法-小灰的算法之旅