作为一名前端爱好者,还是希望把自己的基础打扎实,本片博文就对基本的数据结构和算法相关知识点进行了简单的梳理。
数据结构概述
学习数据结构和算法可以使用任意一种语言进行描述,本篇博客以java为例来总结,如果不是特别了解java语言,可以先进行基本的语法学习哦!
什么是数据结构?
先贴官方概念:
数据结构是指相互之间存在着一种或者多种关系的数据元素的集合和该集合元素中数据元素之间的关系组成。
那么撇开枯燥晦涩难懂的定义,用通俗化的语言来讲,
无非就是数据与数据之间的关系。
数据的分类
1. 数据的存储结构(指的是将数据存储在计算机的内存中)
(1)顺序存储结构
按照计算机实际物理内存的地址依次存储数据,地址是连续的,可以类比一下“排队购票”。
(2)链式存储结构
数据与数据之间是通过指针地址联系在一起的,地址可以不连续存储,可以类比一下银行排号机,每位用户都有自己的业务办理号码,大家不用站着排队,随便坐哪里都可以。
(3)两种方式的区别如下
数据插入:链式更方便
2. 数据的逻辑结构(指的是数据与数据之间的逻辑关系)
(1)集合结构
集合结构中的数据元素同属于一个集合,他们之间是并列的关系,可以理解为每个数据元素都是独立的一份,除此之外并无其他任何关系。
(2)线性结构
线性结构中的元素存在**一对一**的相互关系
(3)树形结构
树形结构中的元素存在**一对多**的相互关系
(4)图形结构
图形结构中的元素存在**多对多**的相互关系
算法概述
什么是算法?
官方定义:是指解题方案的准确而完整的描述,是一系列解决问题的清晰指令,算法代表着用系统的方法描述解决问题的策略机制。
算法的特性
- 输入(算法可以有0~多个输入)
- 输出(每一个算法至少要有一个输出)
- 有穷性(每个算法要在有穷的步骤或者说有穷的执行时间内运算)
- 确定性(算法结果是确定的)
- 可行性(每个算法应该要解决一些实际的问题)
算法的基本要求
- 正确性:我们写的算法要能够正确的解决一些问题
- 可读性:类比于程序的可读性,写出来的东东要能够让别人看得懂
- 健壮性:算法应该考虑全面的情况
- 时间复杂度:算法执行占用的时间
- 空间复杂度:算法运行占用的内存
接下来看一小段代码(算法没有最好的,只有最适合的算法)
public class Add{
//实现从1加到100
public static void main(String[] args){
int total = 0;
int end = 100;
//算法一:使用循环
for(int i=0;i<=end;i++){
total += i;
}
//算法二:使用数学思想
total = (1+end)*end/2;
//打印结果
System.out.println(total);
}
}
线性结构
数组 => 采用顺序存储
数组的基本使用(添加、删除等操作)
**数组的基本使用(以下代码段中数组的长度不可变)**
1.创建数组
int[] arr1 = new int[3];
创建数组的同时为元素赋值
int[] arr2 = new int[]{90,50,56,42};
2.获取数组长度
int length1 = arr1.length; //3
3.访问数组中的元素:数组名[下标] 注意:下标从0开始,最大可以取到数组长度减一
int element = arr1[0];
4.为数组中的元素赋值
arr1[0] = 99;
5.遍历数组,
for(int i=0;i<length1;i++){,
System.out.println("arr1 element"+i+":"+arr1[i]);
}
**解决长度不可变问题(可将一下代码封装成函数以便复用)**
//往数组中添加目标元素
public static void main(String[] args){
int[] arr = new int[]{9,8,7};
//快速查看数组中的元素
System.out.println(Arrays.toString(arr));
//要加入数组的目标元素
int target = 6;
//先创建一个新的数组,新数组的长度为原数组长度加1
int newArr = new int[arr.length+1];
//将原数组中的数据全部复制到新数组中
for(int i=0;i<arr.length;i++){
newArr[i] = arr[i];
}
System.out.println(Arrays.toString(newArr)); //[9,8,7,0]
//把目标元素放到新数组的最后
newArr[arr.length] = target;
System.out.println(Arrays.toString(newArr)); //[9,8,7,6]
//用新数组替换旧数组
arr = newArr;
System.out.println(Arrays.toString(arr)); //[9,8,7,6]
}
//根据数组下标删除元素
public static void main(String[] args){
int[] arr = new int[]{9,8,7,6,5,4};
//需要删除的元素下标
int index = 3;
//创建一个新数组,新数组长度为原数组长度减一
int[] newArr = new int[arr.length-1];
//复制原数组中除要删除的下标所对应的元素之外的其他元素到新数组中
for(int i=0;i<newArr.length;i++){
//要删除的元素之前的元素直接赋值
if(i<index){
newArr[i] = arr[i];
}else{
//要删除的元素之后的元素向后取值
newArr[i] = arr[i+1];
}
}
//新数组赋给原数组
arr = newArr;
}
查找算法
- 线性查找(依次对比,并且返回下标,如果找不到则返回-1,所以此算法效率较低)
public static void main(String[] args){
int[] arr = new int[]{2,8,5,4,7,6,9};
int target = 8;
//目标元素所在的下标
int index = -1;
//遍历数组
for(int i=0;i<arr.length;i++){
if(arr[i] == target){
index = i;
break;
}
}
//打印目标元素所在的下标
System.out.println("index:"+index);
}
- 二分法查找(优点:对比次数少;缺点:只应用于有序数组)
public static void main(String[] args){
//给定数组是有序的
int[] arr = new int[]{1,2,3,4,5,6,7,8,9};
//记录开始位置
int start = 0;
//记录结束位置
int end = arr.length-1;
//记录中间位置
int mid = (start+end)/2;
//目标元素
int target = 8;
//记录目标位置
int index = -1;
//循环查找,但是循环次数不确定
while(true){
//判断中间位置的元素是不是要查找的目标元素
if(arr[mid] == target){
index = mid;
break;
}else {
if(arr[mid]>target){
end= mid-1;
}else{
start= mid+1;
}
mid = (start+end)/2;
}
}
System.out.println("index:"+index);
}
面向对象的数组
创建一个面向对象的数组,这个对象中包含有数组的方法和属性,比如说数组的以下方法:
add() insert() get() delete()
public class MyArray{
private int[] elements; //目标数组:用于存储数据的数组
//构造方法
public MyArray(){
elements = new int[0];
}
//获取数组长度的方法
public int size(){
return elements.length;
}
//往数组的末尾添加元素
public void add(int target){
int[] newArr = new int[elements.length+1];
for(int i=0;i<elements.length;i++){
newArr[i] = elements[i];
}
newArr[elements.length] = target;
elements = newArr;
}
//打印所有元素到控制台
public void show(){
System.out.println(Arrays.toString(elements));
}
//获取指定位置的元素
public int get(int index){
return elements[index];
}
//替换指定位置的元素
public void set(int index,int target){
//判断下标是否越界
if(index<0 || index>elements.length-1){
throw new RuntimeException("下标越界");
}
elements[index] = target;
}
//插入一个元素到指定位置
public void insert(int index,int target){
//创建一个新数组
int[] newArr = new int[elements.length+1];
for(int i=0;i<elements.length;i++){
if(i<index){
newArr[i] = elements[i];
}else{
newArr[i] = elements[i+1];
}
}
newArr[index] = target;
elements = newArr;
}
//根据下标删除元素
public void del(int index){
//判断下标是否越界
if(index<0 || index>elements.length-1){
throw new RuntimeException("下标越界");
}
int[] newArr = new int[elements.length-1];
for(int i=0;i<newArr.length;i++){
if(i<index){
newArr[i] = elements[i];
}else{
newArr[i] = elements[i+1];
}
}
elements = newArr;
}
//线性查找方法
public int search(int target){
for(int i=0;i<elements.length;i++){
if(elements[i]==target){
return i;
}
}
return -1;
}
//二分法查找
public int binarySearch(int target){
int start = 0;
int end = elements.length-1;
int mid = (start+end)/2;
while(true){
//什么情况下没有这个元素?
if(start>=end){
return -1;
}
if(elements[mid]==target){
return mid;
}else{
if(elements[mid]>target){
end = mid-1;
}else{
start = mid+1;
}
mid = (start+end)/2;
}
}
}
//测试
public static void main(String[] args){
MyArray arr = new MyArray();
int size = arr.size();
System.out.println(size); //0
//其余方法测试,可以自行书写
}
}
栈(先进后出FILO) => 采用顺序存储
那么什么是栈呢?
你可以把栈想象成为一个箱子,然后往箱子中放书,一本一本放,这个过程可以称为入栈,最上边的书我们称之为栈顶元素。
话不多说,上代码实现栈
public class MyStack{
//栈的底层我们使用数组来存储数据
int[] elements;
public MySatck(){
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];
//创建一个新的数组
int[] newArr = new int[elements.length-1];
//把原数组中除了最后一个元素的其他元素都放入新数组中
for(int i=0;i<newArr.length;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;
}
//测试
public static void main(String[] args){
MyStack ms = new MyStack();
//压入数据
ms.push(8);
ms.push(7);
ms.pop();
ms.pop();
}
}
队列(先进先出FIFO) => 采用顺序存储
那么什么是队列呢?可以直接类比队列
还是直接撸代码理解:
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 void poll(){
//把数组中的第0个元素取出来
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;
//返回第0个元素
return element;
}
//在主方法中自行进行方法的测试
public static void main(String[] args){
......
}
}
单链表 => 采用链式存储
可以把链式存储的单链表想象成为火车,一节车厢除了要承载乘客之外,还需要连接下一节车厢。
单链表的基本操作,还是撸代码~~~
//一个节点
public class Node{
//节点内容
int data;
//下一个节点指针
Node next;
public Node(int data){
this.data = data;
}
//为节点追加节点
public Node append(Node node){
//this.next = node; //不完善,所以采用以下代码可实现依次追加
//当前节点
Node currentNode = this;
while(true){
//取出下一个节点
Node nextNode = currentNode.next;
//如果下一个节点为null,就跳出
if(nextNode==null){
break;
}
//将下一个节点赋给当前节点
currentNode = nextNode;
}
//循环找到下一节点为空的那个节点,并将要追加的节点赋值给当前节点的下一节点
currentNode.next = node;
//将自己返回出去,可以实现jQuery中连续调用方法的效果
return this;
}
//判断当前节点是否是最后一个节点
public boolean isLast(){
return next==null;
}
//获取下一个节点
public Node next(){
return this.next;
}
//获取某一节点的数据
public int getData(){
return this.data;
}
public static void main(String[] args){
//创建节点
Node n1 = new Node(1);
Node n2 = new Node(2);
Node n3 = new Node(3);
n1.append(n2);
n2.append(n3);
System.out.println(n1.next()); //n2
System.out.println(n2.next()); //n3
System.out.println(n3.next()); //null
}
}
单链表的删除
//节点的删除
public void removeNext(){
//取出下下一个节点
Node newNext = next.next;
//将下下一个节点赋值给当前节点的next
this.next = newNext;
}
//显示所有节点信息
public void show(){
Node currentNode = this;
while(true){
System.out.println(currentNode.data+" ");
currentNode = currentNode.next;
//如果是最后一个节点
if(currentNode==null){
break;
}
}
}
单链表的插入
//插入到当前节点之后
public void insertAfter(Node node){
//取出下一个节点,作为下下一个节点
Node nextNext = next;
//把新节点作为当前节点的下一个节点
this.next = node;
//把下下一个节点设置为新节点的下一个节点
node.next = nextNext;
}
循环链表 =>采用链式存储结构
循环链表没有最后一个节点,其形式上的最后一个节点的next指针指向第一个节点,以下是循环链表的实现
public class LoopNode{
//节点内容
int data;
//下一个节点
LoopNode next = this;
public LoopNode(int data){
this.data = data;
}
//插入到当前节点之后
public void insertAfter(LoopNode node){
//取出下一个节点,作为下下一个节点
Node nextNext = next;
//把新节点作为当前节点的下一个节点
this.next = node;
//把下下一个节点设置为新节点的下一个节点
node.next = nextNext;
}
//节点的删除
public void removeNext(){
//取出下下一个节点
Node newNext = next.next;
//将下下一个节点赋值给当前节点的next
this.next = newNext;
}
}
循环双链表=>采用链式存储结构
每一个节点都会记录自己的上一个节点和下一个节点,这里讲解循环双链表,所以没有最后一个节点
public class DoubleNode{
//上一个节点
DoubleNode pre = this;
//下一个节点
DoubleNode next = this;
//节点数据
int data;
public DoubleNode(int data){
this.data = data;
}
//增加节点
public void addAfter(DoubleNode node){
//原来的下一个节点
DoubleNode nextNext = next;
this.next = node;
node.pre = this;
node.next = nextNext;
nextNext.pre = node;
}
//获取下一个节点
public DoubleNode getNextNode(){
return this.next;
}
//获取上一个节点
public DoubleNode getPreNode(){
return this.pre;
}
}
递归
递归:在一个方法(函数)的内部调用该方法(函数)本身的编程方式
public static void main(String[] args){
print(3); //3 2 1
}
public static void print(int i){
if(i>0){
System.out/println(i);
print(i-1);
}
//print(i-1); //循环调用,导致栈溢出,因为没有递归出口
}
递归应用之斐波那契
//斐波那契数列:1 1 2 3 5 8 13 21 34 55.......
//实现输出斐波那契数列的第n项
public static int febonacci(int i){
if(i==1 || i==2){
return 1;
}else{
return febonacci(i-1)+febonacci(i-2);
}
}
递归应用之汉诺塔问题
游戏规则:把所有圆环从最左边的柱子都套到最右边的柱子上;每次只能移动一个圆环,大的圆环不能压在小的圆环上。
/**
* @param n 共有n个盘子
* @param from 开始的柱子
* @param in 中间的柱子
* @param to 目标柱子
* 无论有多少个盘子,我们都可以看成只有两个盘子
*/
public static hanoi(int n,char from, char in,char to){
if(n==1){
System.out.println("第一个盘子从"+from+"移动到"+to);
}else{
//移动上面的所有盘子到中间位置
hanoi(n-1,from,to,in);
//移动下边的盘子
System.out.println("第"+n+"个盘子从"+from+"移动到"+to);
//把上边的所有盘子从中间移动到目标位置
hanoi(n-1,in,from,to);
}
}