算法概述:
算法=输入+程序+输出
针对某个特点问题的解决方案
先有解决问题的主体思路
功能分解,逐步求精
算法也有好坏之分,主要使用O进行标识
时间复杂度:算法执行所需要的时间
空间复杂度:算法执行所需要的空间
基础算法:
排序:
简单排序:冒泡,插入,选择
高级排序:希尔,归并,堆,计数,快速
查询:
二分查找
数据结构概述:
就是数据在内存中储存的方式
在内存中物理储存方式,
数组
链表
逻辑上的储存方式
线性结构
线性表
栈
队列
数结构
二叉树
查询树
红黑树
平衡树
B树
哈希表
图
排序算法:
三种简单排序的区别,时间复杂度都是0(n^2)
冒泡:
两个相邻的比较,一步步把最大的数字换到数组最后,知道排序完;
遍历次数多,交换次数也多
选择
从未排序的集合中选择最大或者最小值,放到已经排序好的集合中
遍历次数多,但交换次数少
插入
从未排序的集合中拿一个数字,从已经排序的集合中找到插入位置,进行插入
遍历次数少,交换次数多
高级排序(算法的速度都比简单排序快)
快速排序
希尔排序
归并排序
堆排序
计数排序
查询算法
传统的查询算法需要遍历整个集合,时间复杂度O(n)
使用二分查找之后,时间复杂度O(logN)
二分查找只能针对有序集合
public static void binarySeach(int[] arrs,int target,int start,int end){
//1,获取数组的中间值下标
int middle = (start+end)/2;
//2,将中间值和目标进行比较
if(target == arrs[middle]){
System.out.println("找到目标,下标是:"+middle);
}else if(target > arrs[middle]){
//从右边开始找
binarySeach(arrs,target,middle+1,end);
}else{
//从左边开始找
binarySeach(arrs,target,start,middle-1);
}
}
添加递归退出条件
if(end<start){//递归退出条件
//没有找到目标
System.out.println("没有找到目标");
return;
}
带返回结果的二分查找
public static Integer binarySeach(int[] arrs,int target,int start,int end){
if(end<start){//递归退出条件
//没有找到目标
//System.out.println("没有找到目标");
return null;
}
//1,获取数组的中间值下标
int middle = (start+end)/2;
//2,将中间值和目标进行比较
if(target == arrs[middle]){
//System.out.println("找到目标,下标是:"+middle);
return middle;
}else if(target > arrs[middle]){
//从右边开始找
return binarySeach(arrs,target,middle+1,end);
}else{
//从左边开始找
return binarySeach(arrs,target,start,middle-1);
}
}
两种物理结构
数组
是内存中连续的空间,在创建的时候大小就确定了
通过下标可以直接获取数组里面的元素
会有扩容问题
链表
在内存中不是连续的空间,大小可以不固定
可以通过下标找元素,但需要一个个查询
新增和删除速度快,没有扩容问题,但是查询速度慢
链表的实现
创建一个节点类
//给链表设计一个节点
public class Node<T>{
T data;//存储的数据
Node next;//指向下一个节点的引用
public Node(T data, Node next) {
this.data = data;
this.next = next;
}
public T getData() {
return data;
}
public void setData(T data) {
this.data = data;
}
}
实现新增和查询方法
//新增方法
public void add(T t){
if(head==null){
head = new Node(t,null);
}else{
Node temp = head;
//找到最后一个节点
while(temp.next!=null){
temp = temp.next;
}
//将新增的节点放入到最后一个节点的next引用
temp.next = new Node(t,null);
}
size++;
}
//查询方法
public T get(int pos){
if(pos>=size){
throw new IndexOutOfBoundsException();
}
int cnt = 0;
Node<T> temp = head;//从头部开始
//向下移动
while(cnt != pos){
temp = temp.next;
cnt++;
}
return temp.getData();
}
指定位置的新增
public void add(int pos, T t) {
//创建新的节点
Node newNode = new Node(t, null);
if(pos == 0){//在头部插入
//先将新的节点的next指向头部
newNode.next = head;
head = newNode;
}else{
//当前位置的
Node current = getNode(pos-1);
//将新节点的下一个位置,执行当前节点的下一个位置
newNode.next = current.next;
//将当前位置的节点指向新增的节点
current.next = newNode;
}
size++;
}
指定位置的删除
//删除指定位置的数据
public void remove(int i) {
if(i==0){
//将头节点直接移动到下一个节点
head = head.next;
}else{
//找到节点
Node current = getNode(i);
Node preNode = getNode(i-1);
//前面的节点,直接指向下后面的节点
preNode.next = current.next;
}
//删除
size--;
}
栈和队列:
栈:先进后出
自定义实现栈
public class MyStack<T> {
MyLinkedList<T> datas = new MyLinkedList<>();
//压入栈
public void push(T t){
//放在链表的尾部
datas.add(t);
}
//出栈
public T pop(){
//获取头部
T t = datas.get(datas.size-1);
datas.remove(datas.size-1);
return t;
}
}
JDK自带的Stack类
Stack<String> myStack = new Stack<>();
myStack.push("aa");
myStack.push("bb");
myStack.push("cc");
System.out.println(myStack.pop());
System.out.println(myStack.pop());
队列:先进先出
自定义实现
MyQueue<String> queue = new MyQueue<>();
queue.add("aa");
queue.add("bb");
queue.add("cc");
System.out.println(queue.poll());
System.out.println(queue.poll());
JDK自带的实现
//数组实现
Queue<String> queue = new ArrayDeque<>();
//链表实现
queue = new LinkedList<>();
树结构:
是一种非线性结构
既有数组查询的效率,又有链表新增和删除的性能
树的基本特点
每棵树有且仅有一个根节点没有父节点
除了根节点,所有节点都只有一个父节点
每个节点都可以有多个子节点,或者没有子节点
最多只有两个子节点的树叫二叉树
链表实现树
实现树节点
public class Node<T>{
T data;//数据
Node<T> left;//左子节点
Node<T> right;//右子节点
}
创建一颗树
//树的根节点
Node root;
//创建一颗树
public void createTree(){
//创建根节点
root = new Node(1);
//左子节点
Node left = new Node(2);
root.setLeft(left);
//右边子节点
Node right= new Node(3);
root.setRight(right);
//左子左节点
Node leftLeft = new Node(4);
left.setLeft(leftLeft);
//左子右节点
Node leftRight = new Node(5);
left.setRight(leftRight);
//右子左节点
Node rightLeft = new Node(6);
right.setLeft(rightLeft);
//右子右节点
Node rightRight = new Node(7);
right.setRight(rightRight);
}
树的遍历
先序遍历:根节点->左节点->右节点
//实现先序遍历
public void preTrival(Node node){
if(node ==null){
return;
}
//直接输出根节点
System.out.print(node.data+",");
//遍历左子节点
preTrival(node.left);
//遍历右子节点
preTrival(node.right);
}
中序遍历:左节点->根节点->右节点
//实现中序遍历
public void midTrival(Node node){
if(node==null){
return;
}
//遍历左子节点
midTrival(node.left);
//打印根节点
System.out.print(node.data+",");
//遍历右子节点
midTrival(node.right);
}
后序遍历:左节点->右节点->根节点
//实现后序遍历
public void postTrival(Node node){
if(node==null){
return;
}
//遍历左子节点
postTrival(node.left);
//遍历右子节点
postTrival(node.right);
//打印根节点
System.out.print(node.data+",");
}
数组实现树
基本规则
使用数组储存
只能构造完全二叉树
左子节点=当前节点下标*2+1
右子节点=当前节点下标*2+2
遍历
//实现先序遍历
public void preTrival(int index){
if(index>= datas.length){
return;
}
//打印根节点
System.out.print(datas[index]+",");
//遍历左子节点
preTrival(2*index+1);
//遍历右子节点
preTrival(2*index+2);
}
//实现先序遍历
public void midTrival(int index){
if(index>= datas.length){
return;
}
//遍历左子节点
midTrival(2*index+1);
//打印根节点
System.out.print(datas[index]+",");
//遍历右子节点
midTrival(2*index+2);
}
堆排序:
使用数组树
构造一个大顶堆(所有的父类节点要比子节点要打,最大的值就会在根节点)
在构造完大顶堆之后,将根节点和最后一个叶子节点交换,并且脱离树结构
再将树结构继续构造大顶堆,然后再和最后一个叶子节点交换,直到排序完毕
查询树:
树既有链表新增和删除的优势,又有数组查询的效率
一般的树是不具备快速查询的能力,二叉树天然支持二分查找,可以实现快速查找
查询树特点:
左子节点要比右子节点小
在构建查询树的时候有可能会出现左右失去平衡的问题
二叉平衡树
是一个特殊的查询树
在构造树的时候会通过旋转算法使树平衡
当左子树与右子树的高度差大于1的时候,树就会失去平衡,要通过旋转算法来平衡
由于平衡树失去平衡的限制太高了,会导致树在旋转的次数增加,降低效率
红黑树(RBTree)
也是一颗特殊的查询树
主要将树节点分为红黑两种颜色
任意节点到每个叶子节点的黑色节点的数量一致,如果不一致会失去平衡进行旋转
无论树的层数多高,每次旋转都不会超过三次
多路查询树:
二叉查询树的问题,在于数据多的时候,树的高度会很高,从而影响性能
多路查找树,不像二叉树之后两子节点,它具备多个子节点,可以有效降低树的高度提高查询速度
多路查找树就是B树,B+树就是在B树的基础上,将所有的叶子节点通过链表链起来
霍夫曼树
是带权重的树
主要用压缩算法
哈希表
哈希表查询速度非常快,可以达到O(1)
主要原理
创建一个Entry用于储存数据的Key和Value值
主要通过hash散列算法,算出要插入的元素位置
然后根据计算出的问题,见Entry插入到对应数组中
基本实现:
public class MyHashTable<K,V> {
Entry<K,V>[] elements ;//用于存储Entry的数组
public MyHashTable(){
//创建Entry的数组
elements = new Entry[10];
}
//计算所在的位置,散列算法
public int hash(K k){
return k.hashCode()%elements.length;
}
//插入
public void put(K k ,V v){
//使用K执行相关散列算法,获取存储在位置
int pos = hash(k);
//创建Entry放入到指定的位置
elements[pos] = new Entry<>(k,v);
}
//获取
public V get(K k){
//通过散列算法获取位置
int pos = hash(k);
//通过位置获取Entry
Entry<K,V> entry = elements[pos];
return entry.getValue();
}
//定义一个Entry用于存储key和value
public static class Entry<K,V>{
K key;
V value;
public Entry(K key, V value) {
this.key = key;
this.value = value;
}
public K getKey() {
return key;
}
public void setKey(K key) {
this.key = key;
}
public V getValue() {
return value;
}
public void setValue(V value) {
this.value = value;
}
}
}
散列冲突问题
由于数组长度是有限的,在计算entry插入的位置的时候,必然会出现冲突问题
解决方法1:开放寻址法:
在发生冲突的时候通过重新计算,或者扩容的方式,来解决冲突问题
好处可以保证Entry实在数组中,可以序列化
缺点:会浪费很多空间,在重新计算的时候会大大影响效率
适合数据量不大的数据
解决方式:链表式
在发生冲突的时候,在冲突的地方通过链表添加新的数据
好处是插入的时候不需要考虑数组扩容问题
缺点是链表逐渐扩大,会导致性能问题
将entry转变成链表
public static class Entry<K,V>{
K key;
V value;
Entry<K,V> next;//指向下个节点的引用
}
修改插入和查询
//插入
public void put(K k ,V v){
//使用K执行相关散列算法,获取存储在位置
int pos = hash(k);
//判断是否发生碰撞
if(elements[pos]==null){
//创建Entry放入到指定的位置
elements[pos] = new Entry<>(k,v);
}else{
Entry temp = elements[pos];
//链表的遍历
while(temp.next!=null){
temp = temp.next;
}
//插入到链表下一个为null的位置
temp.next = new Entry<>(k,v);
}
}
//获取
public V get(K k){
//通过散列算法获取位置
int pos = hash(k);
//通过位置获取Entry
Entry<K,V> entry = elements[pos];
//判断Entry的key的值是否一致
if(entry.getKey().equals(k)){
return entry.getValue();
}else{
//遍历链表,根据key判断
while(entry.next!=null){
entry = entry.next;
//判断key值是否相同
if(entry.getKey().equals(k)){
return entry.getValue();
}
}
}
return null;
}
JDK中的哈希表:
在jdk早期哈希表主要是Hashtable
之后为了提高效率,jdk提供了HashMap
没有使用锁了
可以对null值进行处理了
提高了hash算法的效率
在处理哈希冲突的时候,也使用链表法,但是在链表数量超过八个,会自动装换成红黑树
图:
图是一种网状结构,和树结构最大的区别就是可以有多个父类节点
图主要是由顶点(Vertex)和边(Edge)组成的
边是顶点和顶点之间的连线
边可以是有向的也可以是无向的
图可以带权重,例如A到B所花的时间
路径是一个顶点到另一个顶点所经过的边的序列
public class MyGraph {
//存储顶点
Vertex[] vertices;
//需要一个矩阵用于存储边
int[][] adjMat;
int verNum;//顶点的数量
public MyGraph(int num){
//初始化数组存储顶点
vertices = new Vertex[num];
//初始化边
adjMat = new int[num][num];
verNum=0;
}
//添加顶点
public void addVertex(String label){
//创建订单对象
Vertex vertex = new Vertex(label);
vertices[verNum++] = vertex;
}
//添加边
public void addEdge(String start,String end){
int sIndex =0;//start这个顶点的坐标
int eIndex =0;
//遍历顶点
for (int i=0;i<vertices.length;i++){
if(vertices[i].label.equals(start)){
//第一个顶点的坐标
sIndex =i;
}
//找到第二个顶点的坐标
if(vertices[i].label.equals(end)){
eIndex =i;
}
}
//定义边
adjMat[sIndex][eIndex] = 1;
adjMat[eIndex][sIndex] = 1;
}
//定义顶点类
public static class Vertex{
String label;//存储顶点的标签
public Vertex(String label){
this.label= label;
}
}
}
两种遍历方法
深度优先遍历:对每个可能的分支路径深入到不能在深为止,而且每个节点只能访问一次
public class MyGraph {
//存储顶点
Vertex[] vertices;
//需要一个矩阵用于存储边
int[][] adjMat;
int verNum;//顶点的数量
public MyGraph(int num){
//初始化数组存储顶点
vertices = new Vertex[num];
//初始化边
adjMat = new int[num][num];
verNum=0;
}
//添加顶点
public void addVertex(String label){
//创建订单对象
Vertex vertex = new Vertex(label);
vertices[verNum++] = vertex;
}
//添加边
public void addEdge(String start,String end){
int sIndex =0;//start这个顶点的坐标
int eIndex =0;
//遍历顶点
for (int i=0;i<vertices.length;i++){
if(vertices[i].label.equals(start)){
//第一个顶点的坐标
sIndex =i;
}
//找到第二个顶点的坐标
if(vertices[i].label.equals(end)){
eIndex =i;
}
}
//定义边
adjMat[sIndex][eIndex] = 1;
adjMat[eIndex][sIndex] = 1;
}
//定义顶点类
public static class Vertex{
String label;//存储顶点的标签
public Vertex(String label){
this.label= label;
}
}
}
广渡优先遍历:
过程检验来说是对每一层的节点依次访问,访问完一层到下一层,每个节点都只访问一次(队列实现)
//广度优先遍历
public void bfs(int pos) {
//创建队列
Queue queue = new LinkedList<>();
queue.add(pos);//入队列
System.out.println(vertices[pos].label);//打印
vertices[pos].isVisited = true;
int current = pos;
while(!queue.isEmpty()) {
current = (int) queue.poll();//出列
for (int i=0;i< vertices.length; i++) {
if(adjMat[current][i]==1&&vertices[i].isVisited==false) {
//入队列
queue.add(i);//入队列
System.out.println(vertices[i].label);//打印
vertices[i].isVisited = true;
}
}
}
}