数据结构简介
数据结构是计算机存储、组织数据的方式。数据结构是指相互之间存在一种或多种特定关系的数据元素的集合。通常情况下,精心选择的数据结构可以带来更高的运行或者存储效率。数据结构往往同高效的检索算法和索引技术有关。
数据结构分类
线性结构:
简单地说,线性结构就是表中各个结点具有线性关系。如果从数据结构的语言来描述,线性结构应该包括如下几点:
1、线性结构是非空集。
2、线性结构有且仅有一个开始结点和一个终端结点
3、线性结构所有结点都最多只有一个直接前趋结点和一个直接后继结点。
线性表就是典型的线性结构,还有栈、队列和串等都属于线性结构。
非线性结构
简单地说,非线性结构就是表中各个结点之间具有多个对应关系。如果从数据结构的语言来描述,非线性结构应该包括如下几点:
1、非线性结构是非空集。
2、非线性结构的一个结点可能有多个直接前趋结点和多个直接后继结点。
在实际应用中,数组、广义表、树结构和图结构等数据结构都属于非线性结构。
数组与稀疏数组
/**
* 稀疏数组:sparseArray (五子棋场景)
* 假设记录在5*10的五子棋盘上的两个棋子
*/
public class ArrayTest {
@Test
public void commonArray(){
int array [][] = new int[5][10];
array[1][2]=1;
array[2][3]=2;
for (int[] ints : array) {
for (int i : ints) {
System.out.print(i);
}
System.out.println();
}
/*
0000000000
0010000000
0002000000
0000000000
0000000000
*/
}
/**
* 使用稀疏数组来简化,
* sparesArray 第一行记录原始数组的大小和不同于大面积相同数据的个数,上述数组就会简化为
* 5 10 2
* 1 2 1
* 2 3 2
*/
原数组中存在大量的无效数据,占据了大量的存储空间,真正有用的数据却少之又少
压缩存储可以节省存储空间以避免资源的不必要的浪费,在数据序列化到磁盘时,压缩存储可以提高IO效率
二维数组转化为稀疏数组/**
/** 二维数组转化为稀疏数组
* @param common
* @return
*/
public int[][] commonToSpares(int[][] common){
//二维数组中最多相同值判断||获取与最多相同值不同的个数
//按照下标依次来记录0-9中每个数出现的次数
int nums [] = new int[10];
int rows = 0;
int cols = 0;
for (int[] row : common) {
rows++;
cols=0;
for (int i : row) {
for (int num = 0 ; num < nums.length ; num++) {
if (i==num){
nums[num]++;
break;
}
}
cols++;
}
}
//获取出现次数最多的数,以及其他数的数量和
int most = 0;
int mostIndex = 0;
int sum = 0;
for (int i = 0; i < nums.length; i++) {
if (nums[i]>most){
most = nums[i];
mostIndex=i;
}
sum = sum + nums[i];
}
int otherSum = sum - most;
//获取到了其他元素的个数以及原来数组的大小,就可以创建稀疏数组了
int spares[][] = new int[otherSum+1][3];
//设置稀疏数组第一行的数据
spares[0][0]=rows;
spares[0][1]=cols;
spares[0][2]=otherSum;
//设置稀疏数组其他值
List<Map<Integer,Integer>> list = new ArrayList<>();
for (int i1 = 0; i1 < rows; i1++) {
for (int i2 = 0; i2 < cols; i2++) {
if (common[i1][i2]!=mostIndex){
Map<Integer,Integer> map = new HashMap<>();
map.put(0,i1);
map.put(1,i2);
map.put(2,common[i1][i2]);
list.add(map);
}
}
}
for (int i = 1; i < spares.length; i++) {
Map<Integer, Integer> integerMap = list.get(i - 1);
spares[i][0]=integerMap.get(0);
spares[i][1]=integerMap.get(1);
spares[i][2]=integerMap.get(2);
}
return spares;
}
稀疏数组转化为二维数组
/**
* 稀疏数组转化为一般数组
* @return
*/
public int[][] sparesToCommon(int [][] spares){
//1.根据稀疏数组第一行可以创建出二维数组的size
int [][] common = new int[spares[0][0]][spares[0][1]];
//2.确定二维数组其他值
for (int i = 1; i < spares.length; i++) {
common[spares[i][0]][spares[i][1]]=spares[i][2];
}
return common;
}
持久化稀疏数组(序列化)
- 准备可以序列化的类
/**
* 持久化稀疏数组
*/
public class SparesArray implements Serializable {
private int[][] sparesArray;
public SparesArray(int[][] sparesArray) {
this.sparesArray = sparesArray;
}
public SparesArray() {
}
public int[][] getSparesArray() {
return sparesArray;
}
public void setSparesArray(int[][] sparesArray) {
this.sparesArray = sparesArray;
}
}
- 写入
/**
* 持久化稀疏数组
* @param sparesArray
* @param file
* @return
*/
public boolean sparesArrayToFiles(SparesArray sparesArray, File file){
boolean flag = false;
try {
ObjectOutputStream obs = new ObjectOutputStream(new FileOutputStream(file));
obs.writeObject(sparesArray);
obs.flush();
flag = true;
obs.close();
} catch (IOException e) {
e.printStackTrace();
}
return flag;
}
- 写出
/**
* 文件转为稀疏数组
* @param file
* @return
*/
public SparesArray fileToSparesArray(File file){
try {
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file));
return (SparesArray) ois.readObject();
} catch (IOException | ClassNotFoundException e) {
return null;
}
}
队列
- 队列是一种特殊的线性表,特殊之处在于它只允许在表的前端(front)进行删除操作,而在表的后端(rear)进行插入操作,和栈一样,队列是一种操作受限制的线性表。进行插入操作的端称为队尾,进行删除操作的端称为队头。
- 可以理解为先进先出
- 队列可以通过数组||链表来实现
数组实现
package queue;
import java.util.Arrays;
public class ArrayToQueue {
private int maxSize;
private int front;
private int rear;
private Object[] queue;
/**
* 初始化队列:根据用户传递的size来确定队列的大小
* @param size
*/
public ArrayToQueue(int size) {
maxSize=size;
queue = new Object[size];
front = -1;
rear = -1;
}
/**
* 判断队列是否是为满
*/
public boolean isFull(){
return rear >= maxSize;
}
/**
* 判断是否为空
*/
public boolean isEmpty(){
return rear==front;
}
/**
*往队列中添加元素
* @param o
*/
public void setValue(Object o){
rear++;
if (isFull()){
throw new IndexOutOfBoundsException("队列已满,无法添加元素");
}else {
queue[rear] = o;
}
}
/**
* 取出队列元素
* @return
*/
public Object getValue(){
if (isEmpty()){
throw new NullPointerException("队列为空,无法取出元素");
}else {
front++;
return queue[front];
}
}
/**
* 展示队列所有元素
*/
public void show(){
if (isEmpty()){
throw new NullPointerException("队列为空,无法取出元素");
}else {
System.out.println(Arrays.toString(queue));
}
}
/**
* 展示头元素
*/
public Object getHead(){
if (isEmpty()){
throw new NullPointerException("队列为空,无法取出元素");
}else {
return queue[front+1];
}
}
}
注意:上述代码中,我们无法实现,取出后空间的复用,所以需要进行优化。
优化:用数组来实现环形队列,可以通过取模来实现。%
package queue;
import java.util.Arrays;
/**
* 数组实现环形队列
*/
public class RingQueue {
private int maxSize;
private int front;
private int rear;
private Object[] queue;
/**
* 初始化队列:根据用户传递的size来确定队列的大小
* @param size
*/
public RingQueue(int size) {
maxSize=size;
queue = new Object[size];
front = 0;
rear = 0;
}
/**
* 判断队列是否是为满
*/
public boolean isFull(){
return (rear+1)%maxSize==front;
}
/**
* 判断是否为空
*/
public boolean isEmpty(){
return rear==front;
}
/**
*往队列中添加元素
* @param o
*/
public void setValue(Object o){
if (isFull()){
throw new IndexOutOfBoundsException("队列已满,无法添加元素");
}else {
queue[rear] = o;
rear = (rear+1)%maxSize;
}
}
/**
* 取出队列元素
* @return
*/
public Object getValue(){
if (isEmpty()){
throw new NullPointerException("队列为空,无法取出元素");
}else {
Object a = queue[front];
front = (front+1)%maxSize;
return a;
}
}
/**
* 展示队列所有元素
*/
public void show(){
if (isEmpty()){
throw new NullPointerException("队列为空,无法取出元素");
}else {
for (int i = front; i <front+maxSize; i++) {
System.out.println(i%maxSize+"--->"+queue[i%maxSize]);
}
}
}
/**
* 展示头元素
*/
public Object getHead(){
if (isEmpty()){
throw new NullPointerException("队列为空,无法取出元素");
}else {
return queue[front];
}
}
}
当然还有其他很多种方式,如:
这种方式很好理解,但是其中每取出一个对列元素就会有有一次遍历,时间复杂度会比较高,推荐第一种实现方案。
package queue;
import java.util.Arrays;
/**
* 实现数组复用除了环形队列还有其他方式,思路太多啦
*/
public class OtherWay {
private int maxSize;
private int front;
private int rear;
private Object[] queue;
/**
* 初始化队列:根据用户传递的size来确定队列的大小
* @param size
*/
public OtherWay(int size) {
maxSize=size;
queue = new Object[size];
front = 0;
rear = 0;
}
/**
* 判断队列是否是为满
*/
public boolean isFull(){
return rear == maxSize;
}
/**
* 判断是否为空
*/
public boolean isEmpty(){
return rear==front;
}
/**
*往队列中添加元素
* @param o
*/
public void setValue(Object o){
if (isFull()){
throw new IndexOutOfBoundsException("队列已满,无法添加元素");
}else {
queue[rear] = o;
rear++;
}
}
/**
* 取出队列元素
* @return
*/
public Object getValue(){
if (isEmpty()){
throw new NullPointerException("队列为空,无法取出元素");
}else {
Object o = queue[front];
//取出元素以后,让数组向下移动一位
for (int i = 0; i < queue.length; i++) {
queue[i]=queue[(i+1)%maxSize];
}
rear--;
return o;
}
}
/**
* 展示队列所有元素
*/
public void show(){
if (isEmpty()){
throw new NullPointerException("队列为空,无法取出元素");
}else {
System.out.println(Arrays.toString(queue));
}
}
/**
* 展示头元素
*/
public Object getHead(){
if (isEmpty()){
throw new NullPointerException("队列为空,无法取出元素");
}else {
return queue[front];
}
}
}
链表
- 链表是一种物理存储单元上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的。链表由一系列结点(链表中每一个元素称为结点)组成,结点可以在运行时动态生成。每个结点包括两个部分:一个是存储数据元素的数据域,另一个是存储下一个结点地址的指针域。相比于线性表顺序结构,操作复杂。由于不必须按顺序存储,链表在插入的时候可以达到O(1)的复杂度,比另一种线性表顺序表快得多,但是查找一个节点或者访问特定编号的节点则需要O(n)的时间,而线性表和顺序表相应的时间复杂度分别是O(logn)和O(1)。
实现一个链表来完成简单的链表增删改的操作。
package linkedList;
/**
* 模拟一个简单的链表,思路:
* 1. 可以定义一个头节点用来指定链表开始位置所在,头节点不发生改变,定义值域以及next域指向
* 2. 初始化头节点(关于头节点要根据业务的需求来决定)
* 3. 添加增删改查的操作
*/
public class SimpleLinkedList {
//链表的初始化
private Student head = new Student(0,"",0);
//添加链表节点
//不考虑对学生id排序的情况下,将新增的链表直接放在next=null的节点的后面
public void addValue(Student newNode){
//设置一个变量动态指向每个节点,方便遍历
Student temp = head;
//遍历
while (true){
if (temp.next==null){
temp.next = newNode;
break;
}
temp = temp.next;
}
}
//排序的情况下进行插入,号码由低到高,号码相同就报错
public void addValueById(Student newNode){
//0 1 5
//辅助变量指向头节点
Student temp = head;
//循环去遍历节点找到适合位置插入
while (true){
if (temp.next==null){ //处理第一次插入元素
temp.next = newNode;
break;
}
if (newNode.id>temp.next.id){ //获取其位置并且插入
newNode.next=temp.next;
temp.next=newNode;
break;
}
if (newNode.id==temp.id||newNode.id==temp.next.id){ //报错
System.out.println("已经存在该节点");
break;
}
temp = temp.next;
}
}
//链表的修改,根据对象的id来进行修改,id不存在就报错
public void updateList(int id ,String name,Double score){
Student temp = head;
while (true){
if (temp.next==null){
System.out.println("链表不存在该元素");
break;
}
if (temp.next.id==id){
temp.next.name=name;
temp.next.score=score;
System.out.println("成功修改为:");
System.out.println(temp.next);
break;
}
temp=temp.next;
}
}
//删除链表
public void deleteNode(int id){
Student temp = head;
while (true){
if (temp.next==null){
System.out.println("不存在该元素");
break;
}
if (temp.next.id==id){
if (temp.next.next!=null){
temp.next=temp.next.next;
break;
}else {
break;
}
}
temp = temp.next;
}
}
//单链表的反转
public void reversalList(){
//初始化该链表,接收头节点
Student temp1 = head.next;
//自定义一个新的头节点,用两个链表的相互转化来实现
Student temp2 = new Student(0,"",0);
if (temp1==null){
System.out.println("链表为空");
}else {
while (true){
Student loop = null;
if (temp1==null){
head.next = null;
head.next = temp2.next;
System.out.println("反转完成");
break;
}
loop = temp2.next;//null,1,2
temp2.next=temp1;//3 2 1
temp1 = temp1.next;//1-2 2-3 3-null
temp2.next.next=loop;//1-=null,2-1-null
}
}
}
//逆序打印链表
public void printlnList(){
Stack<Student> stack = new Stack<>();
Student temp = head;
if (temp.next==null){
System.out.println("链表为空");
}else {
while (true){
if (temp.next==null){
break;
}
stack.add(temp.next);
temp=temp.next;
}
}
while (stack.size()>0){
System.out.println(stack.pop());
}
}
//查看链表元素
public void showList(){
//设置一个变量动态指向每个节点,方便遍历
Student temp = head;
if (temp.next==null){
System.out.println("链表为空");
}
//遍历
while (true){
if (temp.next==null){
break;
}
System.out.println(temp.next);
temp = temp.next;
}
}
}
class Student{
Integer id;
String name;
double score;
Student next;
public Student(Integer sId, String sName, double sScore) {
this.id = sId;
this.name = sName;
this.score = sScore;
}
@Override
public String toString() {
return id+"---->"+name+"----->"+score;
}
}
双向链表
- 上述单向链表中,在查询的时候,总是只能朝一个方向去查找。
- 单项链表不可以实现自我删除,每次删除都需要依靠前面的一个节点进行删除的操作。
- 双向链表可以实现自我删除。
双向链表也叫双链表,是链表的一种,它的每个数据结点中都有两个指针,分别指向直接后继和直接前驱。所以,从双向链表中的任意一个结点开始,都可以很方便地访问它的前驱结点和后继结点。一般我们都构造双向循环链表。
package linkedList;
/**
* 模拟双向链表的增删改查功能
*/
public class BidirectionalLinkedList {
//初始化一个头结点
public Person head = new Person(0, "0");
//默认的方式增加,直接增加到链表的后面
public void addValueByDefaultWey(Person newNode) {
Person temp = head;
while (true) {
if (temp.next == null) {
temp.next = newNode;
newNode.pre = temp;
break;
}
temp = temp.next;
}
}
//显示链表所有的节点
public void showNodes() {
Person temp = head;
if (temp.next != null) {
while (true) {
if (temp.next == null) {
break;
}
System.out.println(temp.next);
temp = temp.next;
}
} else {
System.out.println("空......");
}
}
//修改链表的数据by id
public void updateData(int id, String name) {
Person temp = head;
while (true) {
if (temp.next == null) {
System.out.println("无该元素");
break;
}
if (temp.next.id == id) {
temp.next.name = name;
break;
}
temp = temp.next;
}
}
//删除节点(自我删除)
public void deleteNode(int id){
Person temp = head;
while (true){
if (temp.next==null){
System.out.println("无该元素");
break;
}
if (temp.id==id){
temp.next.pre=temp.pre;
temp.pre.next=temp.next;
break;
}
temp=temp.next;
}
}
}
class Person{
public int id;
public String name;
public Person next;
public Person pre;
public Person(int id, String name) {
this.id = id;
this.name = name;
}
@Override
public String toString() {
return "Person{" +
"id=" + id +
", name=" + name +
'}';
}
}
约瑟夫(环)问题—单向链表来解决
问题描述:丢手帕 :由1-n个人围成一个圈,从k开始报数,每次报数1,报到m时,那个人出列,在从出列人后一个人开始报数,依次类推,到所有人退出,排列出开始到结束依次出队列的一个编号。
package linkedList;
import java.util.Scanner;
/**
* 使用单向链表
* 解决约瑟夫问题
*/
public class JosephusProblem {
Children temp = null;
Children head = null;
//创建一个新的列表用来接受弹出元素
Children [] children = null;
int length = 0;
int index = 0;
//根据传入的人数先建立一个环形链表
public void setList(int num){
//获取链表的长度
length = num;
index = num;
for (int i = 0; i < num; i++) {
if (temp==null){
temp = new Children(i);
head = temp;
}else {
temp.next=new Children(i);
temp = temp.next;
}
}
children = new Children[num];
//让链表的最后一个元素指向最前面第一个元素,形成一个闭环
temp.next=head;
for (int i = 0; i < num; i++) {
System.out.println(head);
head = head.next;
}
}
/**
* 确定弹出链表的元素
* @param id: 代表从哪个人开始数
*/
public void getNode(int id){
Children begin = null;
//通过遍历找到这个id代表的元素的前一个元素
if (id<=0){
for (int i = 1; i < length; i++) {
head=head.next;
}
begin=head;
}else {
for (int i = 0; i < length; i++) {
if (head.id==(id-1)){
begin=head;
break;
}
head=head.next;
}
}
System.out.println("选择的元素节点是:"+begin.next);
Scanner scanner = new Scanner(System.in);
while (true){
if (length==0){
//表示元素已经全部弹出
System.out.println("没有元素了");
break;
}
System.out.println("请输入你要报数几次后弹出该元素");
int num = scanner.nextInt()%length;
if (num==0){
num=length;
}
for (int i = 1; i < num; i++) {
begin = begin.next;
}
System.out.println("出列:"+begin.next);
children[index-length]=begin.next;
begin.next=begin.next.next;
length--;
}
System.out.println("出队列的顺序依次为");
for (Children child : children) {
System.out.println(child);
}
}
}
class Children{
//代表编号
public int id;
public Children(int id) {
this.id = id;
}
public Children next;
@Override
public String toString() {
return "Children{" +
"id=" + id +
'}';
}
}