数据结构基本类型的实现代码
数组/顺序表
数组是可以在内存中连续存储多个元素的结构,在内存中的分配也是连续的,数组中的元素通过数组下标进行访问,数组下标从 0 开始。
优点:适用于查询
- 按照索引查询元素速度快
- 按照索引遍历数组方便
缺点:
- 数组的大小固定后就无法扩容了
- 数组只能存储一种类型的数据
- 添加、删除的操作慢,因为要移动其他的元素
适用场景:
- 频繁查询,对存储空间要求不大,很少增加和删除的情况
数组的插入、删除操作:
- 最坏情况:在位置 0 插入、删除,时间复杂度 O(n)
- 平均情况:插入和删除都需要移动表的一半的元素
- 最优情况:在表的高端进行插入、删除,没有元素需要移动,时间复杂度 O(1)
数组的添加、删除、插入都要借助新数组,然后替换掉原数组数据
数组相关操作在后续其他类型的实现中利用到,故此处不做过多编写,详情看后续其他结构的实现中数组的应用。
数组中的二分查找算法实现
/**
* TODO
* 二分查找法-适用于有序数组
* @author Redamancy
* @version 1.0
* @since jdk 1.8
*/
public class TestBinarySearch {
public static void main(String[] args) {
//目标数组
int[] arr = new int[]{1,2,3,4,5,6,7,8,9};
//目标元素
int target = 8;
//记录开始位置
int begin = 0;
//记录结束位置
int end = arr.length-1;
//记录中间位置
int mid = (begin+end)/2;
//记录目标位置
int index = -1;
//循环查找
while (true){
//判断中间的这个元素是否是要查找的元素
if(arr[mid] == target){
index = mid;
break;
}else {
//判断中间这个元素与目标元素的大小
if(arr[mid] > target){
//把新结束位置调整为原中间位置的前一个位置
end = mid-1;
}else {
//把新开始位置调整为原中间位置的后一个位置
begin = mid+1;
}
//取出新的中间位置
mid = (begin+end)/2;
}
}
System.out.println("index:"+index);
}
}
链表
使用对象存储 data(结点数据) , next(结点后继)
链表是物理存储单元上非连续的、非顺序的存储结构,数据元素的逻辑顺序是通过链表的指针地址实现,每个元素包含两个结点,一个是存储元素的数据域(内存空间),另一个是指向下一个结点的指针域。根据指针的指向,链表能形成不同的结构,例如单链表,双向链表,循环链表等。
简概:
- 链表是一种物理存储单元上非连续、非顺序的存储结构
- 数据元素的逻辑顺序通过链表中的指针链接次序实现
- 链表由一系列结点组成,结点不必在内存中相连
- 结点:由数据部分Data和链部分 Next 组成;
Next 链指向下一个结点;
结点不必在内存中相连
优点:适用于修改
- 链表是很常用的一种数据结构,不需要初始化容量,可以任意添加删除元素
- 添加或删除元素时只需要改变前后两个元素结点的指针域指向地址即可,所以添加、删除元素很快
缺点:
因为含有大量的指针域,占用空间较大
查找元素需要遍历链表来查找,非常耗时
适用场景:
数据量较小,需要频繁添加,删除操作的场景
单链表代码实现
/**
* TODO
* 单链表实现
* @author Redamancy
* @version 1.0
* @since jdk 1.8
*/
public class Node {
//节点内容
int data;
//下一个节点
Node next;
public Node(int data){
this.data = data;
}
//为节点追加节点-无限追加放到最后
public void append(Node node){
// this.next =
//当前节点
Node currentNode = this;
//循环向后找
while (true){
//取出下一个节点
Node nextNode = currentNode.next;
if(nextNode==null){
//当前节点已经没有后继了
//跳出循环,将当前节点的后继设置成新添加的节点
break;
}
//当前的后继节点赋给当前节点
currentNode = nextNode;
}
//把想要追加的节点追加为找到的当前节点的下一个节点
currentNode.next = node;
}
//在当前节点后插入新节点
public void after(Node node){
//取出当前节点的后继,作为下下一个节点
Node nodeNext = next;
//将新节点设置为当前节点的后继
this.next = node;
//将下下一个节点设置为新节点的后继
node.next = nodeNext;
}
//获取下一个节点
public Node next(){
return this.next;
}
//获取节点数据
public int getData(){
return this.data;
}
//删除当前节点的后继
//单向链表没法删除自己,因为无法获取前驱
public void removeNext(){
//需要先取出后继的下一个节点
// 否则删除后继后无法建立线性关系
Node newNext = next.next;
//把后继的下一个节点设置为当前节点的下一个节点
this.next = newNext;
}
//获取节点信息
public void show(){
Node current = this;
while (true){
System.out.println(current.data+" ");
//取出下一个节点
current = current.next;
if(current == null){
break;
}
}
}
}
单向循环链表代码实现
在用代码实现循环链表时,最重要的一步是:
LoopNode next = this;即默认自身“成环”
/**
* TODO
* 循环链表
* @author Redamancy
* @version 1.0
* @since jdk 1.8
*/
public class LoopNode {
//重要:LoopNode next = this
//节点内容
int data;
//下一个节点,一个默认是自己-自成环
LoopNode next = this;
public LoopNode(int data){
this.data = data;
}
//为当前节点插入节点
public void after(LoopNode node){
//取出当前节点后继,作为下下一个节点
LoopNode nextNext = next;
//把新节点设置为当前节点的后继
this.next = node;
//把下下一个节点设置成新节点的后继
node.next = nextNext;
}
//删除当前节点的后继
public void removeNext(){
//取出当前节点的后继的后继
LoopNode nextNext = next.next;
this.next = nextNext;
}
//获取节点信息-实际上循环会一直打印
public void show(){
LoopNode start = this;
LoopNode current = this;
while (true){
System.out.println(current.data+" ");
//取出下一个节点
current = current.next;
if(current.data == start.data){
break;
}
}
}
}
双向循环链表代码实现
/**
* TODO
* 双向循环链表
* @author Redamancy
* @version 1.0
* @since jdk 1.8
*/
public class DoubleNode {
//前驱节点
DoubleNode pre = this;
//后继节点
DoubleNode next = this;
//节点内容
int data;
public DoubleNode(int data){
this.data = data;
}
//插入新节点
public void after(DoubleNode node){
//找到当前节点的后继
DoubleNode nextNode = next;
//将新节点设置成当前节点的后继
this.next = node;
//新节点的前驱设置成当前节点
node.pre = this;
//将当前节点的原后继设置成新节点的后继
node.next = nextNode;
//将当前节点的原后继的前驱设置成新节点
nextNode.pre = node;
}
//递归解决汉诺塔问题。一次只能移动一个
public void hanoi(int n,String from,String in,String to){
//只有一个盘子
if(n == 1){
System.out.println("第1个盘子从"+from+"移动到"+to);
}else{
//无论有多少个盘子,都认为只有两个
//上面的所有盘子和最下面的一个盘子
//1.移动上面所有盘子到中间位置
hanoi(n-1,from,to,in);
//2.移动最下面的盘子
System.out.println("第"+n+"个盘子从"+from+"移到"+to);
//把上面的所有盘子从中间位置移到目标位置
hanoi(n-1,in,from,to);
}
}
}
以上代码的测试类:
/**
* TODO
* 链表测试
* @author Redamancy
* @version 1.0
* @since jdk 1.8
*/
public class test {
public static void main(String[] args) {
System.out.println("=====单链表测试=====");
Node node = new Node(1);
node.append(new Node(2));
node.append(new Node(3));
node.after(new Node(4));
node.show();
System.out.println("移除后继");
node.removeNext();
node.show();
System.out.println("=====循环链表测试=====");
//不建议使用 show() 因为后继永远都有会不停重复打印
LoopNode ln = new LoopNode(1);
ln.after(new LoopNode(2));
ln.next.after(new LoopNode(3));
ln.next.after(new LoopNode(4));
ln.show();
System.out.println("移除后继");
ln.next.removeNext();
ln.show();
System.out.println("=====递归解决汉诺塔=====");
DoubleNode dn = new DoubleNode(2);
dn.hanoi(3,"A","B","C");
}
}
栈
栈可以由数组实现。
- 栈是一种特殊的线性表,仅能在线性表的一端操作,栈顶允许操作,栈底不允许操作。
- 栈的特点:先进后出/后进先出。
从栈顶放入元素的操作叫入栈;
从栈顶取出元素的操作叫出战 - 栈的实现:栈是一个表,能实现表的方法都能实现栈
代码实现
/**
* TODO
* 栈的实现
* 取元素只能取最后一个
* @author Redamancy
* @version 1.0
* @since jdk 1.8
*/
public class MyStack {
//栈的底层使用数组实现
int[] elements;
public MyStack(){
elements = new int[0];
}
//压入元素
public void push(int element){
int[] newArr = new int[elements.length+1];
//把原数组的元素复制到新数组
for(int i=0;i<elements.length;i++){
newArr[i] = elements[i];
}
//把添加的元素放入新数组中
newArr[elements.length] = element;
//新数组替换旧数组
elements = newArr;
}
//取出栈顶元素
public int pop(){
//栈中无元素
if(elements.length==0){
throw new RuntimeException("stack is empty");
}
//取最后一个元素
int element = elements[elements.length-1];
//创建新的数组:长度-1
int[] newArr = new int[elements.length-1];
//原数组除了最后一个元素都放入新数组中
for(int i=0;i<elements.length-1;i++){
newArr[i] = elements[i];
}
//替换数组
elements = newArr;
//返回栈顶元素
return element;
}
//查看栈顶元素-不删除
public int peek(){
//栈中无元素
if(elements.length==0){
throw new RuntimeException("stack is empty");
}
return elements[elements.length -1];
}
//判断栈是否为空
public boolean isEmpty(){
return elements.length==0;
}
}
测试类
/**
* TODO
* 栈的相关操作测试
* @author Redamancy
* @version 1.0
* @since jdk 1.8
*/
public class test {
public static void main(String[] args) {
MyStack ms = new MyStack();
ms.push(9);
ms.push(8);
ms.push(7);
int i = ms.pop();
int j = ms.pop();
System.out.println(i + "-" + j);
System.out.println(ms.isEmpty());
}
}
队列
数组实现
队列与栈一样,也是一种线性表,不同的是,队列可以在一端添加元素,在另一端取出元素。即:先进先出
从一端放入元素的操作称为入队,取出元素称为出队
简概:
- 队列元素只能从队列尾(后端rear)插入,从队列头(前端font)访问和删除
- 队列操作:
插入:enqueue 入队,队尾(末端)
删除:dequenu 出队,对头(前端),删除也在对头 - 队列又叫 FIFO(First in First Out),先进先出(普通队列)
注:优先队列中,元素被赋予优先级,具有最高优先级的元素最先被删除。 - 使用场景:因为队列先进先出的特点,在多线程阻塞队列管理中非常适用
代码实现
/**
* TODO
* 队列的实现
* @author Redamancy
* @version 1.0
* @since jdk 1.8
*/
public class MyQueue {
int[] elements;
public MyQueue(){
elements = new int[0];
}
//入队
public void add(int element){
int[] newArr = new int[elements.length+1];
//把原数组的元素复制到新数组
for(int i=0;i<elements.length;i++){
newArr[i] = elements[i];
}
//把添加的元素放入新数组中
newArr[elements.length] = element;
//新数组替换旧数组
elements = newArr;
}
//出队-第一个元素
public int poll(){
int element = elements[0];
int[] newArr = new int[elements.length-1];
for(int i=0;i<newArr.length;i++){
//除去原数组第一个元素
newArr[i] = elements[i+1];
}
//替换数组
elements = newArr;
return element;
}
public boolean isEmpty(){
return elements.length==0;
}
}
测试类
/**
* TODO
* 队列相关操作的测试
* @author Redamancy
* @version 1.0
* @since jdk 1.8
*/
public class test {
public static void main(String[] args) {
MyQueue mq = new MyQueue();
mq.add(9);
mq.add(8);;
mq.add(7);
System.out.println(mq.poll());
}
}
哈希表/散列表
散列表,也叫哈希表,是根据关键码、值(key 和 value)直接进行访问的数据结构,通过 key 和 value 来映射到集合中的一个位置,这样就可以很快找到集合中的对应元素。
哈希表在应用中也是比较常见的,就如 Java 中有些集合类就是借鉴了哈希原理构造的,例如 HashMap,HashTable 等,利用 hash 表的优势,对于集合的查找元素时非常方便。然而,因为哈希表是基于数组衍生出来的数据结构,在添加、删除元素方面是比较慢的,所以很多时候需要用到一种数组链表来做,也就是拉链法。
拉链法是数组结合链表的一种结构,较早前的 hashMap 底层的存储就是采用这种结构,直到 jdk1.8之后才换成了 数组+红黑树 的结构。
散列函数的设计:
- 设计原则:计算简单,分布均匀
- 直接定址法:将数字放到“对应”位置(直接将数字放到数组对应下标)。
分布不均匀,如用电话号码为键,11位数的数组长度。。。 - 平方取中法:先将数字平方,取中间(如 13 * 13 = 169 -> 取6)
可能出现中间数相同,将前一个数据覆盖 - 取余法:设置一个数,逐个取余
也可能出现余数相同,将前一个数据覆盖 - 随机数法:随便放,存储地址 = random()
不严谨,不科学,不建议
代码实现(取余法为例)
/**
* TODO
* 哈希表-本质数组
* @author Redamancy
* @version 1.0
* @since jdk 1.8
*/
public class HashTable {
private StuInfo[] data = new StuInfo[100];
/**
* 直接存址法
* 向散列表中添加元素
* @param stuInfo 学生信息对象
*/
public void put(StuInfo stuInfo){
//调用散列函数获取存储位置-根据年龄
int index = stuInfo.hashCode();
// int index1 = stuInfo.hashCode1();
//添加元素-将对象信息存到以年龄为下标的位置
data[index] = stuInfo;
}
//通过 传入的对象判断查找哪一个下标的数据
public StuInfo get(StuInfo stuInfo){
return data[stuInfo.hashCode()];
}
}
/**
* TODO
* 以学生年龄来取余数分配位置
* @author Redamancy
* @version 1.0
* @date 2020/9/12 - 0:44
* @since jdk 1.8
*/
public class StuInfo {
//学生年龄
int age;
//学生年龄对应人数
int nums;
//构造方法
public StuInfo(int age,int nums){
this.age = age;
this.nums = nums;
}
public StuInfo(int age) {
this.age = age;
}
//散列函数-根据年龄-直接存址法
public int hashCode(){
return age;
}
//取余法,因为设置的年龄都是10~20范围内
public int hashCode1(){
return age%10;
}
@Override
public String toString() {
return "StuInfo{" +
"age=" + age +
", nums=" + nums +
'}';
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public int getNums() {
return nums;
}
public void setNums(int nums) {
this.nums = nums;
}
}
/**
* TODO
* 散列表函数设计/哈希表
* @author Redamancy
* @version 1.0
* @date 2020/9/12 - 0:43
* @since jdk 1.8
*/
public class TestHashTable {
public static void main(String[] args) {
StuInfo s1 = new StuInfo(16,3);
StuInfo s2 = new StuInfo(17,11);
StuInfo s3 = new StuInfo(18,23);
StuInfo s4 = new StuInfo(19,24);
StuInfo s5 = new StuInfo(20,9);
HashTable ht = new HashTable();
ht.put(s1);
ht.put(s2);
ht.put(s3);
ht.put(s4);
ht.put(s5);
//想要获取的目标数据
StuInfo target = new StuInfo(18);
StuInfo info = ht.get(target);
System.out.println(info);
}
}
散列冲突的解决方法
开放地址法
- 线性探测法:若当前想放的位置有值了,就往后寻找最近一个空位置,将值放入。
可能会造成某一块区域数据很密集(如 上面代码中,18岁的学生有很多个,从“18”该放的位置起数据很密集),不推荐。 - 二次探测法:若当前想放的位置有值了,就往后查找 i^2 个位置是否为空,否则就继续指导找到空的位置(如:当前想放在位置 1,位置不为空,往后移动 1 ^ 2 位置查看是否为空;结果位置 2 有值,再往后移动 2 ^ 2 个位置,结果2 + 2 ^ 2 = 6 即位置6 为空,放下数值。否则继续前面操作)。
可能会造成很多位置的浪费,即数组长度过长中间却很多空位置。 - 再哈希法:需要两个(或三个及以上)散列函数。假设用函数一发现当前位置不为空,不继续调用函数一寻找空位置,而是转调用函数二寻找空位置
链地址法
图
数组 + 邻接矩阵实现
图是由结点的又穷集合 V 和边的集合 E 组成。其中,为了与树型结构加以区别,在图结构中常常将结点称为顶点,边是顶点的有序偶对,若两个顶点之间存在一条边,就表示这两个顶点具有相邻关系。
相关名词:顶点、边、邻接(两点之间一条边即可到达)、路径、有向图和无向图,带权图
- 图是一种教线性表和树更复杂的数据结构
- 在线性表中,元素之间仅有线性关系
- 在树结构中,数据之间有明显的层次关系
- 在图形结构中,结点之间的关系可以是任意的,图中任意两个元素之间都可能相关。
图的遍历方式
- 深度优先搜索算法——栈实现
- 广度优先搜索算法——队列实现
代码实现(深度优先遍历)
/**
* TODO
* 图类,表示顶点关系
* @author Redamancy
* @version 1.0
* @since jdk 1.8
*/
public class Graph {
private Vertex[] vertex;
private int currentSize;
//用来表示顶点之间的关系(矩阵图)
public int[][] adjMat;
//存放顶点数据的栈
private MyStack stack= new MyStack();
//存放当前遍历的下标
private int currentIndex;
public Graph(int size) {
vertex = new Vertex[size];
adjMat = new int[size][size];
}
/**
* 深度优先遍历
* 栈实现
*/
public void dfs(){
//把第 0 个顶点标记为已访问
vertex[0].visited = true;
//把第 0 个顶点的下标加入栈
stack.push(0);
//打印顶点的值
System.out.println(vertex[0].getValue());
out:while (!stack.isEmpty()){
for(int i = currentIndex+1;i < vertex.length;i++){
//与下一个有边且未访问
if(adjMat[currentIndex][i]==1 && vertex[i].visited==false){
//压入栈
stack.push(i);
vertex[i].visited = true;
System.out.println(vertex[i].getValue());
continue out;
}
}
//弹出栈顶元素
stack.pop();
//修改当前位置为栈顶元素的位置
if(!stack.isEmpty()){
currentIndex = stack.peek();
}
}
}
/**
* 向图中加入一个顶点
* @param v 要添加的顶点
*/
public void addVertex(Vertex v){
vertex[currentSize++] = v;
}
/**
* 为顶点之间添加一条边
* @param v1 顶点1
* @param v2 顶点2
*/
public void addEdge(String v1,String v2){
//找出两个顶点下标
int index1 = 0;
for(int i = 0;i < vertex.length;i++) {
if (vertex[i].getValue().equals(v1)) {
index1 = i;
break;
}
}
int index2 = 0;
for(int i = 0;i < vertex.length;i++){
if(vertex[i].getValue().equals(v2)) {
index2 = i;
break;
}
}
//在矩阵中对应位置赋值 1 ,代表两点直接互通有路
adjMat[index1][index2] = 1;
adjMat[index2][index1] = 1;
}
}
/**
* TODO
* 顶点类
* @author Redamancy
* @version 1.0
* @date 2020/9/12 - 1:32
* @since jdk 1.8
*/
public class Vertex {
private String value;
//标志该顶点是否已经遍历过
public boolean visited;
public String getValue() {
return value;
}
public void setValue(String value) {
this.value = value;
}
public Vertex(String value) {
this.value = value;
}
}
测试类
/**
* TODO
* 图测试
* @author Redamancy
* @version 1.0
* @date 2020/9/12 - 1:48
* @since jdk 1.8
*/
public class TestGraph {
public static void main(String[] args) {
Vertex v1 = new Vertex("A");
Vertex v2 = new Vertex("B");
Vertex v3 = new Vertex("C");
Vertex v4 = new Vertex("D");
Vertex v5 = new Vertex("E");
Graph g = new Graph(5);
g.addVertex(v1);
g.addVertex(v2);
g.addVertex(v3);
g.addVertex(v4);
g.addVertex(v5);
//添加边
g.addEdge("A","C");
g.addEdge("B","C");
g.addEdge("A","B");
g.addEdge("B","D");
g.addEdge("B","E");
for(int[] a:g.adjMat){
System.out.println(Arrays.toString(a));
}
//深度优先遍历图
g.dfs();
}
}
堆
- 堆是一种比较特殊的数据结构,可以被看做是一颗树的数组对象
- 特点:
堆中某个结点的值总是不大于或不小于其父结点
堆总是一颗完全二叉树
堆有大顶堆,小顶堆
以下是一颗 大顶堆
堆可用于堆排序,堆排序的原理跟实现可以参考编写的其他文章
原理:堆排序原理
实现:代码实现