概述
数据结构与算法关系
程序=数据结构+算法
有些实际问题通过数据结构就能够搞定,有些较困难的问题需要数据结构+算法才能搞定,要想学好算法,数据结构必须要扎实。
数据结构分类
线性结构+非线性结构
线性结构:分为顺序存储和链式存储(顺序表示内存分配的地址是连续的,链式的地址是不连续的)
线性结构有:
- 数组
- 队列
- 链表
- 栈
非线性结构有:
- 二维数组、多维数组
- 广义表
- 树
- 图
稀疏数组
当一个数组中大部分元素为同一个值,可以使用稀疏数组来保存该数组
稀疏数组的a[0]用来保存原数组的行和列,还有有效数据个数
然后稀疏数组的后边的值记录不同值的行、列、值
通过下边应用场景的图片应该能看的很清楚稀疏数组的含义
应用场景:五子棋小游戏,用二维数组代表棋盘,没有下的地方都为0,黑子1,白子2,那么我们只需要用稀疏数组表示棋盘的行和列、下子的个数,每个子的在棋盘上的行列位置和值(值表示黑白子)
实现了二维数组转为稀疏数组、稀疏数组恢复为二维数组
public class test {
public static void main(String[] args) {
/*
二维数组
*/
int chessArray[][]=new int[11][11];
chessArray[1][2]=1;
chessArray[2][3]=2;
//输出一下二维数组
System.out.println("二维数组");
for (int[] ch:chessArray){
for (int val:ch){
System.out.print(val+"\t");
}
System.out.println();
}
/*
* 二维数组转换稀疏数组
* */
//统计有效数据
int sum=0;
for (int i=0;i<chessArray.length;i++){
for(int j=0;j<chessArray[0].length;j++){
if(chessArray[i][j]!=0){
sum++;
}
}
}
//创建稀疏数组sparseArray
int[][] sparseArray=new int[sum+1][3];
//给稀疏数组第一行赋值
sparseArray[0][0]=chessArray.length;
sparseArray[0][1]=chessArray[0].length;
sparseArray[0][2]=sum;
int count=0;
for (int i=0;i<chessArray.length;i++){
for(int j=0;j<chessArray[0].length;j++){
if(chessArray[i][j]!=0){
count++;
sparseArray[count][0]=i;
sparseArray[count][1]=j;
sparseArray[count][2]=chessArray[i][j];
}
}
}
//输出稀疏数组
System.out.println("转换得到的稀疏数组:");
for (int[] ch:sparseArray){
for (int val:ch){
System.out.print(val+"\t");
}
System.out.println();
}
/*
将稀疏数组转换回二维数组
*/
//获取行信息
int row=sparseArray[0][0];
//获取列信息
int line=sparseArray[0][1];
//获取有效数据数量
int count2=sparseArray[0][2];
//创建二维数组
int[][] chessArray2=new int[row][line];
for(int i=1;i<count2+1;i++){
int row2=sparseArray[i][0];
int line2=sparseArray[i][1];
chessArray2[row2][line2]=sparseArray[i][2];
}
//输出一下二维数组
System.out.println("转换后的二维数组");
for (int[] ch:chessArray){
for (int val:ch){
System.out.print(val+"\t");
}
System.out.println();
}
}
}
队列
Java中的栈和队列
Stack类 已经过时的栈操作类
Queue类 队列类
Deque类 双端队列(可以进行栈操作)
队列和双端队列都是借口,双端队列是队列的子接口,LinkedList是队列的实现类,
Java的栈和队列的操作都可以通过LinkedList来实现
队列是线性结构,可以使用数组或链表来实现,可以看到下边这张图,表示了队列在增加和取出时的工作状态(数组模拟队列),即增加时增大rear,取出时增加front(先进先出),当rear<MaxSize -1时,可以往里边增加,当rear==MaxSize -1,队列满,无法加入
front含义:指向队列第一个元素的前一个位置,初值为-1
rear含义:指向队列尾部,初值为-1
队列满的条件:rear== MaxSize -1
队列空的条件:rear== front
ArrayQueue类队列实现
public class test2 {
public static void main(String[] args) {
ArrayQueue queue=new ArrayQueue(5);
queue.addQueue(1);
queue.addQueue(4);
queue.addQueue(8);
queue.addQueue(8);
System.out.print(queue.getQueue()+"\t");
System.out.println(queue.headQueue());
queue.showQueue();
}
}
//编写一个ArrayQueue类
class ArrayQueue{
private int maxSize;//数组最大容量
private int front;//队列头
private int rear;//队列尾部
private int[] arr;
//构造器创建队列
public ArrayQueue(int maxSize) {
this.maxSize = maxSize;
arr=new int[maxSize];
front=-1;//指向队列头部
rear=-1;//指向队列尾部
}
//队列是否满
public boolean isFull(){
return rear==maxSize-1;
}
//队列是否为空
public boolean isEmpty(){
return rear==front;
}
//添加数据到队列
public void addQueue(int n){
//判断队列是否满
if(isFull()){
throw new RuntimeException("队列满");
}
arr[++rear]=n;
}
//获取队列数据,出队列
public int getQueue(){
if(isEmpty()){
//抛出异常
throw new RuntimeException("队列空");
}
return arr[++front];
}
//
public void showQueue(){
if(isEmpty()){
System.out.println("队列空");
}
for (int i=front+1;i<=rear;i++){
System.out.println(arr[i]+"\t");
}
}
//显示头数据,仅显示,不出队列
public int headQueue(){
if(isEmpty()){
throw new RuntimeException("队列空");
}
return arr[front+1];
}
}
循环队列
上边的实现虽然实现了队列,但数组是一次性的,front和rear都只能往队列尾部走,到尾就不能用了
下边将队列优化成环形队列
front含义更改:指向队列的第一个元素,初值为0
rear含义更改:指向队列最后元素的下一个位置,初值为0
队列满的条件是(rear+1)%maxSize=front
- 当front=0 rear=maxSize-1时满,此时符合这个式子,其他情况下满,front<rear且挨着的(rear+1=front)(这里不懂可以看下上边的图想一下循环队列满的情况),所以也满足这个式子
队列空的条件是 rear==front
public class test3{
public static void main(String[] args) {
ArrayQueue2 queue=new ArrayQueue2(5);
queue.addQueue(1);
queue.addQueue(4);
queue.addQueue(8);
queue.addQueue(8);
System.out.print(queue.getQueue()+"\t");
System.out.println(queue.headQueue());
queue.showQueue();
}
}
//编写一个ArrayQueue类
class ArrayQueue2{
private int maxSize;//数组最大容量
private int front;//指向队列头部
private int rear;//指向队列尾部下一个位置
private int[] arr;
//构造器创建队列
public ArrayQueue2(int maxSize) {
this.maxSize = maxSize;
arr=new int[maxSize];
}
//队列是否满
public boolean isFull(){
return (rear+1)%maxSize==front;
}
//队列是否为空
public boolean isEmpty(){
return rear==front;
}
//添加数据到队列
public void addQueue(int n){
//判断队列是否满
if(isFull()){
throw new RuntimeException("队列满");
}
arr[rear]=n;//rear指向的是尾元素的下一个,所以直接赋值给rear位置
rear=(rear+1)%maxSize; //当到尾端时,取模就会回到0处,实现了循环队列
}
//获取队列数据,出队列
public int getQueue(){
if(isEmpty()){
//抛出异常
throw new RuntimeException("队列空");
}
//保存要去除的值
int temp=arr[front];
//移动front
front=(rear+1)%maxSize;
//返回
return temp;
}
//展示全部
public void showQueue(){
if(isEmpty()){
System.out.println("队列空");
}
for (int i=front;i<front+size();i++){
System.out.println(arr[i%maxSize]+"\t");
}
}
//求出当前队列有效数据的个数
public int size(){
return (rear+maxSize-front)%maxSize;
}
//显示头数据,仅显示,不出队列
public int headQueue(){
if(isEmpty()){
throw new RuntimeException("队列空");
}
return arr[front];
}
}
线性表
链表部分因为我在学javaSE的时候已经写过了关于手动实现arrayList和LinkedList的博客,况且链表部分也比较简单,能看懂上边那张图的直接看下边两篇中的代码问题不大。
顺序表
手写实现ArrayList
import jdk.nashorn.internal.runtime.regexp.joni.constants.OPCode;
import java.util.Arrays;
public class test4 {
public static void main(String[] args) {
MyArrayList<String>myArrayList=new MyArrayList<>();
//测试输出空串
System.out.println(myArrayList.toString());
//测试add
myArrayList.add("good");
myArrayList.add("well");
//测试输出非空串
System.out.println(myArrayList.toString());
//测试删除
myArrayList.remove(0);
System.out.println(myArrayList.toString());
//测试set
myArrayList.set(0,"haha");
System.out.println(myArrayList.toString());
System.out.println(myArrayList.size());
}
}
class MyArrayList<E>{
private int size;
private Object[]arr;
public MyArrayList(int size) {
this.size = size;
arr=new Object[size];
}
public MyArrayList() {
arr=new Object[16];
}
//add
public void add(E e){
//数组扩容
if(size>=arr.length){
Object[] temp=new Object[arr.length+ (arr.length)>>1];
for (int i=0;i< arr.length;i++){
temp[i]=arr[i];
}
arr=temp;
}
arr[size++]=e;
}
//set
public void set(int index,E e){
checkIndex(index);
arr[index]=e;
}
//size
public int size(){
return size;
}
//checkIndex
public void checkIndex(int index){
if(index<0||index>size){
throw new RuntimeException("无效索引---"+index);
}
}
//remove
public void remove(int index){
checkIndex(index);
Object[]temp=new Object[arr.length];
for (int i=0;i<size;i++){
if(i<index){
temp[i]=arr[i];
}else if(i>index){
temp[i-1]=arr[i];
}
}
arr=temp;
size--;
}
//toString
@Override
public String toString() {
StringBuilder sb=new StringBuilder("[");
for (int i=0;i<size;i++){
sb.append(arr[i]+",");
}
if(sb.length()==1){
sb.append("]");
}else{
sb.setCharAt(sb.length()-1,']');
}
return sb.toString();
}
}
测试add超出数组长度,没问题
链表
手写实现LinkedList(单链表实现和双链表实现)
单链表实现
import java.util.LinkedList;
public class test7 {
public static void main(String[] args) {
MySingleLinkedList<String> mySingleLinkedList=new MySingleLinkedList<>();
mySingleLinkedList.add("hello");
mySingleLinkedList.add("world");
System.out.println(mySingleLinkedList.get(0));
System.out.println(mySingleLinkedList.get(1));
System.out.println(mySingleLinkedList.toString());
mySingleLinkedList.remove(0);
System.out.println(mySingleLinkedList.toString());
mySingleLinkedList.add("hello");
mySingleLinkedList.add("world");
mySingleLinkedList.add(2,"haha");
System.out.println(mySingleLinkedList.toString());
System.out.println(mySingleLinkedList.get(1));
}
}
class MySingleLinkedList<E>{
class Node{
Node next;
E element;
public Node(E element) {
this.element = element;
}
}
private Node head;
private int size;
//add
public void add(E e){
Node newNode=new Node(e);
if(head==null){
head=newNode;
}else{
Node temp=head;
while(temp.next!=null){
temp=temp.next;
}
temp.next=newNode;
}
size++;
}
//add(int index)
public void add(int index,E e){
checkIndex(index);
Node newNode=new Node(e);
if(index==0){
newNode.next=head;
head=newNode;
}else{
Node pre=getNode(index-1);
newNode.next=pre.next;
pre.next=newNode;
}
size++;
}
//get
public E get(int index){
return getNode(index).element;
}
//checkIndex
public void checkIndex(int index){
if(index<0||index>size){
throw new RuntimeException("无效索引"+"---"+index);
}
}
//toString
@Override
public String toString() {
StringBuilder sb=new StringBuilder("[");
Node temp=head;
for(int i=0;i<size;i++){
sb.append(get(i)+",");
}
if(sb.length()==1){
sb.append("]");
}else{
sb.setCharAt(sb.length()-1,']');
}
return sb.toString();
}
//remove
public void remove(int index){
checkIndex(index);
if(index==0){
head=head.next;
}else{
Node pre=getNode(index-1);
Node next=getNode(index+1);
pre.next=next;
}
size--;
}
//getNode
public Node getNode(int index){
checkIndex(index);
Node temp=head;
for (int i=0;i<index;i++){
temp=temp.next;
}
return temp;
}
//size
public int size(){
return size;
}
}
双链表实现
public class test5 {
public static void main(String[] args) {
MyLinkedList<String> myLinkedList=new MyLinkedList<>();
myLinkedList.add("hello");
myLinkedList.add("world");
System.out.println(myLinkedList.toString());
myLinkedList.remove(0);
System.out.println(myLinkedList.toString());
myLinkedList.add("hello");
myLinkedList.add("world");
myLinkedList.add(2,"haha");
System.out.println(myLinkedList.toString());
System.out.println(myLinkedList.get(1));
}
}
class MyLinkedList<E>{
Node head;
private int size;
//添加元素节点
public void add(E e){
Node newNode=new Node(e);
if(head==null){
head=newNode;
size++;
}else{
Node temp=head;
while(temp.next!=null){
temp=temp.next;
}
temp.next=newNode;
newNode.previous=temp;
size++;
}
}
//插入元素节点
public void add(int index,E e){
checkIndex(index);
Node newNode=new Node(e);
if(index==0){
newNode.next=head;
head.previous=newNode;
head=newNode;
size++;
}else{
Node pre=getNode(index-1);
Node next=getNode(index);
newNode.next=next;
next.previous=newNode;
newNode.previous=pre;
pre.next=newNode;
size++;
}
}
//获取某节点 这里是为上边插入服务的
public Node getNode(int index){
Node temp=head;
for(int i=0;i<index;i++){
temp= temp.next;
}
return temp;
}
//toString
@Override
public String toString() {
StringBuilder sb=new StringBuilder("[");
Node temp=head;
if(size==0){
sb.append("]");
}else{
for(int i=0;i<size;i++){
sb.append(temp.element+",");
temp=temp.next;
}
sb.setCharAt(sb.length()-1,']');
}
return sb.toString();
}
//检查索引有效性
public void checkIndex(int index){
if(index<0||index>size){
throw new RuntimeException("索引无效");
}
}
//删除元素
public void remove(int index){
checkIndex(index);
if(index==0){
head=head.next;
}else{
Node pre=getNode(index-1);
Node next=getNode(index+1);
pre.next=next;
next.previous=pre;
}
size--;
}
//获取某个节点的值
public Object get(int index){
checkIndex(index);
return getNode(index).element;
}
//返回size
public int size(){
return size;
}
}
class Node{
Node previous;
Node next;
Object element ;
public Node(Object element) {
this.element = element;
}
public Node(Node previous, Node next, Object element) {
this.previous = previous;
this.next = next;
this.element = element;
}
}
在此总结一下LinkedList和ArrayList的区别
从底层的实现可以看到,这两个的功能互相都有,但是在效率方面有比较大的差异,ArrayList的增删功能(插入功能我的文章里面没有实现,JDK里面是有的)每次都要重新定义数组,并且遍历复制,而LinkedList只需要改变链表元素的next,previous即可,在查找元素方面,ArrayList可以直接根据下表查找,而LinkedList在底层需要遍历链表。
在List下面的实现类还有一个Vector,它的优点是很安全,但是效率低,一般多线程的时候使用。
环形链表:约瑟夫问题
public class test8 {
public static void main(String[] args) {
CircleSingleLinkedList circleSingleLinkedList=new CircleSingleLinkedList(5);
circleSingleLinkedList.boyOut(3,2);
}
}
class CircleSingleLinkedList{
//人数
private int num;
//第一个元素
private Boy first;
public CircleSingleLinkedList(int num) {
this.num = num;
}
//创建环形链表
public void createList(){
for(int i=1;i<num+1;i++){
Boy newBoy=new Boy(i);
if(i==1){
first=newBoy;
first.next=first;
}else{
Boy temp=first;
while(temp.next!=first){
temp=temp.next;
}
temp.next=newBoy;
newBoy.next=first;
}
}
}
//打印出圈顺序
public void boyOut(int k,int m){
createList();
Boy temp=first;
Boy temp2=first;
if(k>num||k<0||first==null){
throw new RuntimeException("无效参数");
}
//temp设置到初始位置
for(int i=1;i<k-1;i++){
temp2=temp2.next;
}
temp=temp2.next;
int count=0;
while(true){
if(temp.next==temp){
System.out.println(temp.no);
break;
}else {
count++;
if (count % m == 0) {
System.out.println(temp.no);
temp2.next=temp.next;
temp=temp.next;
}else {
temp2=temp;
temp=temp.next;
}
}
}
}
}
class Boy{
int no;
Boy next;
public Boy(int no) {
this.no = no;
}
}
经典面试题
单链表有效节点个数
已知单链表head,求有效节点个数(这里我的单链表头结点有值)
public int getLength(Node head){
if(head==null){
return 0;
}else{
int count=0;
Node temp=head;
while(temp.next!=null){
temp=temp.next;
count++;
}
return count+1;//循环最后一个没执行加1
}
}
查找单链表倒数第k个(index从1开始)
已知单链表头结点,得到倒数第k个数(k=1,2…)
先算出有效长度size,再一个循环获取到第size-k个数(从0开始)
//有效数据个数
public static int getLength(Node head){
if(head==null){
return 0;
}else{
int count=0;
Node temp=head;
while(temp.next!=null){
temp=temp.next;
count++;
}
return count+1;
}
}
public static Node findLastIndexNode(Node head,int index){
if(head==null){
return null;
}
int size=getLength(head);
if(index<=0||index>size){
return null;
}else{
Node temp=head;
for(int i=0;i<size-index;i++){
temp=temp.next;
}
return temp;
}
}
链表反转(头结点有值的单链表)
已知单链表头结点,将本链表反转
//单链表反转
public static Node reverseList(Node head){
//链表为空或者只有一个元素,直接返回就好
if(head==null||head.next==null){
return head;
}else{
//让head不受任何的影响,搞一个新元素代替原来头结点的元素作用(反转后该节点在最后一个,如果是原来head则没法赋值成第一个)
Node first=new Node(head.element);
first.next=head.next;
//新的链表头结点
Node head2=new Node(0);
//辅助节点
Node temp=first;
//记录下一个节点
Node next; // world hello haha world
while(temp.next!=null){
next=temp.next;
if(head2.next!=null){
temp.next=head2.next;
}else{
temp.next=null;
}
head2.next=temp;
temp=next;
}
//最后一个节点加进去
temp.next=head2.next;
head2.next=temp;
//赋值给head
head.next=head2.next.next;
head.element=head2.next.element;
return head;
}
}
从尾到头打印单链表
已知单链表头结点,从尾部开始打印单链表
使用栈这个数据结构,将链表的元素先压入栈中,然后再取出来打印,就是逆序的了(这里用的java库的Stack类)
public static void reversePrint(Node head){
Stack<String>stack=new Stack<>();
Node temp=head;
if(head==null){
throw new RuntimeException("head为空");
}
while(temp.next!=null){
stack.add((String) temp.element);
temp=temp.next;
}
stack.add((String) temp.element);
int size=stack.size();
for(int i=0;i<size;i++){
System.out.print(stack.pop()+"\t");
}
}
有序链表合并
//传入两个链表的头结点(头结点有值)
public static MySingleLinkedList mergeList(Node head,Node head2){
if(head==null||head==null){
return null;
}
Node temp=head;
Node temp2=head2;
MySingleLinkedList<Integer> list=new MySingleLinkedList();
while(true){
if(temp!=null&&temp2!=null){
if((Integer)temp.element>=(Integer)temp2.element){
list.add((Integer) temp2.element);
temp2=temp2.next;
}else{
list.add((Integer) temp.element);
temp=temp.next;
}
}else if(temp==null&&temp2!=null){
list.add((Integer) temp2.element);
temp2=temp2.next;
}else if(temp2==null&&temp!=null){
list.add((Integer) temp.element);
temp=temp.next;
}else{
break;
}
}
return list;
}
结果:[1,2,3,4]
栈
栈stack 栈顶Top 栈底bottom 出栈pop 入栈push peek
Java中的栈和队列
Stack类 已经过时的栈操作类
Queue类 队列类
Deque类 双端队列(可以进行栈操作)
队列和双端队列都是借口,双端队列是队列的子接口,LinkedList是队列的实现类,
Java的栈和队列的操作都可以通过LinkedList来实现
栈的数组实现
public class stack {
public static void main(String[] args) {
ArrayStack stack=new ArrayStack(5);
stack.push(1);
stack.push(4);
stack.push(8);
stack.push(8);
System.out.println(stack.pop());
System.out.println(stack.pop());
System.out.println(stack.peek());
}
}
class ArrayStack{
private int top=-1;
private int[] stack;
private int maxSize;
public ArrayStack(int maxSize) {
this.maxSize = maxSize;
stack=new int[maxSize];
}
//push
public void push(int val){
if(isFull()){
throw new RuntimeException("队列满");
}else{
stack[++top]=val;
}
}
//pop
public int pop(){
if(isEmpty()){
throw new RuntimeException("队列空");
}else{
int value=stack[top--];
return value;
}
}
public int peek(){
if(isEmpty()){
throw new RuntimeException("队列空");
}else{
return stack[top];
}
}
//isFull
public boolean isFull(){
return top==maxSize-1;
}
//isEmpty
public boolean isEmpty(){
return top==-1;
}
}
栈的链表实现
public class test7 {
public static void main(String[] args) {
ListStack stack=new ListStack();
stack.push(1);
stack.push(4);
stack.push(8);
stack.push(8);
System.out.println(stack.pop());
System.out.println(stack.pop());
System.out.println(stack.peek());
}
}
class ListStack{
private MySingleLinkedList<Integer>stack=new MySingleLinkedList<>();
int top=-1;
//push
public void push(int val){
stack.add(val);
top++;
}
//peek
public int peek(){
if(isEmpty()){
throw new RuntimeException("栈空");
}
int val=stack.get(stack.size()-1);
return val;
}
//pop
public int pop(){
if(isEmpty()){
throw new RuntimeException("栈空");
}
int val=stack.get(stack.size()-1);
stack.remove(stack.size()-1);
top--;
return val;
}
//isEmpty
public boolean isEmpty(){
return top==-1;
}
}
class Node{
Node next;
Object element;
public Node(Object element) {
this.element = element;
}
}
class MySingleLinkedList<E>{
public Node getHead(){
return head;
}
private Node head;
private int size;
//add
public void add(E e){
Node newNode=new Node(e);
if(head==null){
head=newNode;
}else{
Node temp=head;
while(temp.next!=null){
temp=temp.next;
}
temp.next=newNode;
}
size++;
}
//add(int index)
public void add(int index,E e){
checkIndex(index);
Node newNode=new Node(e);
if(index==0){
newNode.next=head;
head=newNode;
}else{
Node pre=getNode(index-1);
newNode.next=pre.next;
pre.next=newNode;
}
size++;
}
//get
public E get(int index){
return (E) getNode(index).element;
}
//checkIndex
public void checkIndex(int index){
if(index<0||index>size){
throw new RuntimeException("无效索引"+"---"+index);
}
}
//toString
@Override
public String toString() {
StringBuilder sb=new StringBuilder("[");
Node temp=head;
for(int i=0;i<size;i++){
sb.append(get(i)+",");
}
if(sb.length()==1){
sb.append("]");
}else{
sb.setCharAt(sb.length()-1,']');
}
return sb.toString();
}
//remove
public void remove(int index){
checkIndex(index);
if(index==0){
head=head.next;
}else{
Node pre=getNode(index-1);
Node next=getNode(index+1);
pre.next=next;
}
size--;
}
//getNode
public Node getNode(int index){
checkIndex(index);
Node temp=head;
for (int i=0;i<index;i++){
temp=temp.next;
}
return temp;
}
//size
public int size(){
return size;
}
}
栈实现计算器
public class test11 {
public static void main(String[] args) {
String expression="2+3*6-4";
//数栈和符号栈
ArrayStack numStack=new ArrayStack(10);
ArrayStack operStack=new ArrayStack(10);
//
int index=0;
char ch=' ';
while(true){
//ch赋值
ch=expression.substring(index,index+1).charAt(0);
//符号
if(operStack.isOper(ch)){
//符号栈非空
if(!operStack.isEmpty()){
//优先级大,直接放
if(operStack.priority(ch)>= operStack.priority(operStack.peek())){
operStack.push(ch);
}else{//优先级小,pop*3
int num1=numStack.pop();
int num2=numStack.pop();
int ch2=operStack.pop();
int cal = operStack.cal(num1, num2, ch2);
numStack.push(cal);
operStack.push(ch);
}
}else{//符号栈空
operStack.push(ch);
}
}else { //数字
String str= String.valueOf(ch);
while(true){
if(index<expression.length()-1) { //未到最后一个
if (operStack.isOper(expression.charAt(index + 1))) {//下一个是符号
numStack.push(Integer.parseInt(str));
break;
} //下一个不是符号
else {
str += expression.charAt(index+1);
index++;
}
}else{ //到最后一个
numStack.push(Integer.parseInt(str));
break;
}
}
}
index++;
if(index>=expression.length()){
break;
}
}
while(true){
//符号栈空,break
if(operStack.isEmpty()){
break;
} else{//pop*3
int num1=numStack.pop();
int num2=numStack.pop();
int ch2=operStack.pop();
int cal = operStack.cal(num1, num2, ch2);
numStack.push(cal);
}
}
//返回res
int res=numStack.pop();
System.out.println("表达式:"+expression);
System.out.println("表达式预算结果:"+res);
}
}
class ArrayStack {
private int top = -1;
private int[] stack;
private int maxSize;
public ArrayStack(int maxSize) {
this.maxSize = maxSize;
stack = new int[maxSize];
}
//push
public void push(int val) {
if (isFull()) {
throw new RuntimeException("队列满");
} else {
stack[++top] = val;
}
}
//pop
public int pop() {
if (isEmpty()) {
throw new RuntimeException("队列空");
} else {
int value = stack[top--];
return value;
}
}
public int peek() {
if (isEmpty()) {
throw new RuntimeException("队列空");
} else {
return stack[top];
}
}
//isFull
public boolean isFull() {
return top == maxSize - 1;
}
//isEmpty
public boolean isEmpty() {
return top == -1;
}
//返回运算符的优先级
//使用数字表示
//数字越大,优先级越高
public int priority(int oper){
if(oper =='*'||oper=='/'){
return 1;
}else if(oper=='+'||oper=='-'){
return 0;
}else{
return -1;//假定目前的表达式只有加减乘除
}
}
//爬蛋是不是一个运算符
public boolean isOper(char val){
return val=='+'||val=='-'||val=='*'||val=='/';
}
//计算方法
public int cal(int num1,int num2,int oper){
int res=0;
switch (oper){
case '+':
res=num2+num1;
break;
case '-':
res=num2-num1; //这里应该是后一个减去前一个
break;
case '*':
res=num2*num1;
break;
case '/':
res=num2/num1;
break;
}
return res;
}
}
前中后缀表达式
前缀表达式又称为波兰表达式,前缀表达式的运算符位于操作数之前
中缀表达式就是我们数学中运算表达式,中缀表达式对于人来讲是最方便的,但是对于计算机来说很麻烦,前边我们实现过程中药判断优先级,±*/,还有各种括号等等。
后缀表达式(逆波兰表达式)一般我们将中缀表达式转换成后缀表达式来计算
中缀表达式:(3+4)* 5 - 6
&&
前缀表达式: - * + 3 4 5 6
从右至左扫描,将6 5 4 3压入栈,下一个是+,从栈中取出3,4,使用+进行运算,得到7,将7压入栈中,
下一个*, 取出5,7,运算得到35,将35压入栈中
下一个-,取出35,6,应该栈顶元素运算在前,35-6=29,运算完毕
&& 前缀和后缀差不多,但是后缀是从左到右扫描,更符合人的逻辑,一般就是将中缀转后缀然后进行计算
后缀表达式:34+56-
从左到右扫描
将3,4压入栈中
遇到+,3+4=7,将7压入栈中,将5入栈
遇到,5*7=35,将35压入栈中,6入栈
遇到-,35-6=29,这个是栈底的在前边
逆波兰表达式计算器
实现思路就按上边的例子来,遇到数字入栈,遇到符号pop两个数字运算然后放入栈中
public static void main(String[] args) {
//先定义一个逆波兰表达式
String suffixExpression="3 4 + 5 * 6 -";
//先将其放入ArrayList中
//将ArrayList传递给一个方法,使用配合栈完成计算
int cal=cal(getListString(suffixExpression));
System.out.println("3 4 + 5 * 6 -");
System.out.println(cal);
}
//将数据和运算符放入ArrayList中
public static List<String> getListString(String suffixExpression){
String[]split=suffixExpression.split(" ");
List<String>list=new ArrayList<>();
for(String str:split){
list.add(str);
}
return list;
}
public static int cal(List<String> list){
//创建栈
Stack<String> stack=new Stack();
for(String item:list){
if(item.matches("\\d+")){ //正则判断多位数
//入栈
stack.push(item);
}else{
//pop出两个数并运算,再入栈
int num1=Integer.parseInt(stack.pop());
int num2=Integer.parseInt(stack.pop());//这个在前
int res;
switch (item){
case "+":
res=num1+num2;
break;
case "-":
res=num2-num1;
break;
case "*":
res=num2*num1;
break;
case "/":
res=num2/num1;
break;
default:
throw new RuntimeException("未知运算符");
}
stack.push(res+"");
}
}
//最后栈中的数据就是运算结果
return Integer.parseInt(stack.pop());
}
中缀转后缀
这里7/8两步实现与上边描述不太一样,因为s2没有出栈操作,所以直接用list来代替栈s2,而且不需要逆序,最后得到list就是正确的后缀表达式
public class test13 {
//将中缀表达式转成后缀表达式
//1+((2+3)*4)-5
//1 2 3 + 4 * + 5 -
public static void main(String[] args) {
String expression="1 + ( ( 2 + 3 ) * 4 ) - 5";
//将中缀表达式放在list中
List<String> list = getListString(expression);
//输出测试一下
System.out.println(list.toString());
//完成转换操作
List<String> parseList = parseSuffixExpressionList(list);
System.out.println(parseList.toString());
}
//将数据和运算符放入ArrayList中
public static List<String> getListString(String suffixExpression){
String[]split=suffixExpression.split(" ");
List<String>list=new ArrayList<>();
for(String str:split){
list.add(str);
}
return list;
}
public static List<String> parseSuffixExpressionList(List<String>list){
//定义两个栈
Stack<String> s1=new Stack();//符号栈
List<String>s2=new ArrayList<>();
//遍历
for (String item:list){
//如果是一个数,加入s2
if(item.matches("\\d+")){
s2.add(item);
}else if(item.equals("(")){
//如果是左括号,直接入栈
s1.push(item);
}else if(item.equals(")")){
//如果是右括号,一次弹出s1的运算符,并压入s2,一直到遇到左括号
while(!s1.peek().equals("(")){
s2.add(s1.pop());
}
s1.pop(); //将碰到的左括号丢弃
}else{ //运算符
//优先级小
//将s1中栈顶运算符加入到s2,然后与新的栈顶运算符相比较,一直到优先级小为止 入s1
while(s1.size()!=0 && priority(s1.peek().charAt(0)) >= priority(item.charAt(0)) ){
s2.add(s1.pop());
}
s1.push(item); //入s1
//优先级大于s1,或者s1为空或栈顶为左括号(这些情况都在上边while循环中剔除了)
//直接压入s1中
}
}
//将s1中全部加到s2
while(s1.size()!=0){
s2.add(s1.pop());
}
return s2;
}
public static int priority(int oper){
if(oper =='*'||oper=='/'){
return 1;
}else if(oper=='+'||oper=='-'){
return 0;
}else{
return -1;//假定目前的表达式只有加减乘除
}
}
}
中缀转后缀计算器
这个就是将中缀转后缀+后缀计算整一起了
import com.sun.corba.se.impl.ior.JIDLObjectKeyTemplate;
import java.util.ArrayList;
import java.util.List;
import java.util.Stack;
public class test14 {
public static void main(String[] args) {
//先定义一个中缀表达式
String expression="( 3 + 4 ) * 5 - 6";
//中缀表达式转化成后缀表达式
List<String>list=parseSuffixExpressionList(getList(expression));
System.out.println("中缀表达式:"+expression);
System.out.println("后缀表达式:"+list);
//后缀表达式进行运算
int val = calSuffixExpressionList(list);
System.out.println(val);
}
public static int calSuffixExpressionList(List<String>list){
Stack<String>stack=new Stack<>();
for(String item:list){
//如果是数字,直接放
if(!isOper(item.charAt(0))){
stack.push(item);
}else {
//如果是字符,pop*2数字 pop符号
int num1= Integer.parseInt(stack.pop());
int num2= Integer.parseInt(stack.pop());
int ch=item.charAt(0);
stack.push(cal(num1,num2,ch)+"");
}
}
return Integer.parseInt(stack.pop());
}
public static int cal(int num1,int num2,int ch){
int res=0;
switch (ch){
case '+':
res=num2+num1;
break;
case '-':
res=num2-num1;
break;
case '*':
res=num2*num1;
break;
case '/':
res=num2/num1;
break;
}
return res;
}
public static List<String> parseSuffixExpressionList(List<String> list) {
//
Stack<String>s1=new Stack<>(); //用来存储符号和括号的
List<String>s2=new ArrayList<>(); //辅助链表
for(String item:list){
if(item.matches("\\d+")){ //如果是数字
s2.add(item);
}else if(isOper(item.charAt(0))){
//如果是符号
//如果符号的优先级小,s1栈顶出栈,放入s2中,一直比较到优先级比栈顶大
//如果栈为空,直接放入s1
//如果优先级大,直接放入s1
//如果遇到左括号,直接放入s1
while(s1.size()!=0&&priority(item.charAt(0)) <= priority(s1.peek().charAt(0))){
s2.add(s1.pop());
}
s1.push(item);
}else if(item.equals("(")){//如果是(
s1.push(item);
}else if(item.equals(")")){//如果是)
while(!s1.peek().equals("(")){
s2.add(s1.pop());
}
s1.pop();
}
}
while(s1.size()!=0){
s2.add(s1.pop());
}
return s2;
}
public static boolean isOper(int oper){
return oper=='+'||oper=='-'||oper=='*'||oper=='/';
}
public static int priority(int oper){
if(oper=='+'||oper=='-'){
return 0;
}else if(oper=='*'||oper=='/'){
return 1;
}else{
return -1;
}
}
public static List<String> getList(String expression){
List<String>list=new ArrayList<>();
String[] split = expression.split(" ");
for (String item:split){
list.add(item);
}
return list;
}
}
堆
什么是堆?
堆是具有以下性质的完全二叉树:
每个节点的值都大于或等于其左右孩子节点的值称为大顶堆
每个节点的值都小于或等于其左右孩子节点的值,称为小顶堆
堆用数组表示:对对中的节点按层从左到右进行标号,映射到数组中
递归
递归就是方法自己调用自己,每调用时传入不同的变量。递归能解决许多复杂问题(通常这些问题的中间过程人的大脑很难捋清楚的,但是趋势和结果是我们可预测的)
迷宫问题
//使用递归回溯来给小球找路
//说明 map表示地图
//i,j表示地图出发点 map[1][1]
//右下角表示结束 map[6][5]
//约定:当map[i][j]为0表示该点没有走过,当为1表示墙,为2表示可以走,3表示已经走过,但是走不通
//走迷宫的策略,先走下面,走不通再走右面,下面走不通走上面,上面走不通走左边
在上面的图中,过程如下
从map[1][1] 先向下走,进入递归(可以走通,继续向下走,走不通,向右面走,走不通,向上走,上面走过了,向左走,走不通,将map[2][1]设置为3)
回溯:返回到map[1][1]的栈中,向右走,进入递归(可以走通),再进入递归(向下走不通,向右走,能走通),再次递归。。。最后到达终点
public class test15 {
public static void main(String[] args) {
//创建一个二维数组,模拟迷宫
int [][]map=new int[8][7];
//使用1表示强
//上下置为1
for(int i=0;i<7;i++){
map[0][i]=1;
map[7][i]=1;
}
//左右全部置为1
for(int i=0;i<8;i++){
map[i][0]=1;
map[i][6]=1;
}
//设置挡板
map[3][1]=1;
map[3][2]=1;
map[2][2]=1;
System.out.println("地图的情况:");
for(int i=0;i<8;i++){
for(int j=0;j<7;j++){
System.out.print(map[i][j]+"\t");
}
System.out.println();
}
getWay(map,1,1);
System.out.println("地图的路径情况:");
for(int i=0;i<8;i++){
for(int j=0;j<7;j++){
System.out.print(map[i][j]+"\t");
}
System.out.println();
}
}
//使用递归回溯来给小球找路
//说明 map表示地图
//i,j表示地图出发点 map[1][1]
//右下角表示结束 map[6][5]
//约定:当map[i][j]为0表示该点没有走过,当为1表示墙,为2表示可以走,3表示已经走过,但是走不通
//走迷宫的策略,先走下面,走不通再走右面,下面走不通走上面,上面走不通走左边
public static boolean getWay(int[][]map,int i,int j){
if(map[6][5]==2){
return true;
}else{
//当前点没有走过
if(map[i][j]==0){
//按照策略来走,先假定该点能走通(后面走不通要回溯)
map[i][j]=2;
if(getWay(map,i+1,j)){ //向下走
return true;
}else if(getWay(map,i,j+1)){ //向右走
return true;
}else if(getWay(map,i-1,j)){ //向上走
return true;
}else if(getWay(map,i,j-1)){//向左走
return true;
}else{
//说明该点走不通
map[i][j]=3;
return false;
}
}else{ //map为1 或 2 或 3
return false;
}
}
}
}
八皇后问题
问题描述:8*8的棋盘中,每一行都放一个皇后,一共八个皇后,任意两个皇后不能在同一条直线上,也不能在同一条斜线上。
public class test16 {
int max=8;
int[]array=new int[max];
static int count=0;
public static void main(String[] args) {
test16 test16=new test16();
test16.check(0);
System.out.println("一共有"+count+"种");
}
//编写方法放置第n个皇后
public void check(int n){
if(n == max){ //n=8
print();
return;
}
//依次放入皇后,并判断是否冲突
for(int i=0;i<max;i++){
//先把当前这个皇后n,放到第一列
array[n]=i;
//放置时判断是否冲突
if(judge(n)){
//不冲突
//放n+1个皇后
check(n+1);
}
//如果冲突,就继续执行循环了,即将第n个皇后放置在本行的后一个位置
}
}
public boolean judge(int n){
for(int i=0;i<n;i++){
//判断第n个是否与前面任意皇后在同一列、判断是否在同一斜线
if(array[i] == array[n]||Math.abs(n-i)==Math.abs(array[n]-array[i])){
return false;
}
}
return true;
}
//将皇后摆放的位置
public void print(){
count++;
for(int i=0;i<array.length;i++){
System.out.print(array[i]+"\t");
}
System.out.println();
}
}
排序
时间、空间复杂度
现在都是用空间换时间了
常见的时间复杂度
常数阶
没有循环就是常数阶,时间复杂度不随变量规模的变化而变化
对数阶
典型的对数阶形式 循环里面变量倍数增长(这个倍数即为底,下边例子倍数=2,T(n)=O(log2n))
int i=1;
while(i<n){
i=i*2;
}
线性阶
循环内i的大小线性增长
for(int i=0;i<max;i++){
System.out.print(array[i]+"\t");
}
线性对数阶
就是将对数阶外边套一个线性阶
平方、立方、多次阶
平方两个线性阶嵌套,立方三层,多次多层。
指数阶
写出指数阶代码,基本就凉凉
直接插入排序
新的元素插入到排序部分中,插入时排序部分全部后移
int arr[]={7,5,6,8,9,1,4,3,2,0};
//直接插入排序是从第二项开始排序,假设这里是递增排序,第二项与第一项比较
//如果,第一项大于第二项,什么也不做,不进入循环,否则第一项就往后移动一位,第二项补前面去
//同理当前项前面如果有小于的,就将他们中间的全部后移,然后插入
for(int i=1;i<arr.length;i++){
int temp=arr[i];
int j=i;
while(j>0&&arr[j-1]>temp){
arr[j]=arr[j-1];
j--;
}
arr[j]=temp;
}
System.out.println(Arrays.toString(arr));
希尔排序
根据gap值多次多组使用插入排序
public static void main(String[] args) {
int arr[]={7,5,6,8,9,1,4,3,2,0}; //10个数
for(int gap=arr.length/2;gap>0;gap/=2){
for(int i=gap;i<arr.length;i++){
int j=i;
while(j-gap>=0 && arr[j-gap]>arr[j]){
swap(arr,j-gap,j);
j-=gap;
}
}
}
System.out.println(Arrays.toString(arr));
}
public static void swap(int []arr,int a,int b){
arr[a]=arr[a]+arr[b];
arr[b]=arr[a]-arr[b];
arr[a]=arr[a]-arr[b];
}
直接选择
每次都在未排序部分找最小值加入排序好的部分
//选择排序
int arr[]={7,5,6,8,9,1,4,3,2,0}; //10个数
for(int i=0;i<arr.length;i++){
for(int j=i+1;j<arr.length;j++){
if(arr[j]<arr[i]){
arr[i]=arr[i]+arr[j];
arr[j]=arr[i]-arr[j];
arr[i]=arr[i]-arr[j];
}
}
}
System.out.println(Arrays.toString(arr));
堆排序
大根堆:每个节点都比其子节点的值大
小根堆:每个节点都比父节点的值大
基本思想
1首先将待排序的数组构造成一个大根堆,此时,整个数组的最大值就是堆结构的顶端
2将顶端的数与末尾的数交换,此时末尾的数为最大值
重复这种操作,最大数不断堆积,得到有序数组
如何构造一个大根堆?
第一次保证0-0(数组中)位置大根堆结构,第二次保证0-1位置大根堆结构,如果进来的数大于父节点,交换位置(通俗点说,比较大的数会依次向上交换,一直到比父节点小)
class Solution {
public static void main(String[] args) {
int arr[]={9,6,8,7,2,5,3,4,1,0};
heapSort(arr);
System.out.println(Arrays.toString(arr));//测试
}
//堆排序
public static void heapSort(int[] arr) {
//构造大根堆
heapInsert(arr);
int size = arr.length;
while (size > 1) {
//固定最大值
swap(arr, 0, size - 1);
size--;
//构造大根堆
heapify(arr, 0, size);
}
}
//构造大根堆(通过新插入的数上升)
public static void heapInsert(int[] arr) {
for (int i = 0; i < arr.length; i++) {
//当前插入的索引
int currentIndex = i;
//父结点索引
int fatherIndex = (currentIndex - 1) / 2;
//如果当前插入的值大于其父结点的值,则交换值,并且将索引指向父结点
//然后继续和上面的父结点值比较,直到不大于父结点,则退出循环
while (arr[currentIndex] > arr[fatherIndex]) {
//交换当前结点与父结点的值
swap(arr, currentIndex, fatherIndex);
//将当前索引指向父索引
currentIndex = fatherIndex;
//重新计算当前索引的父索引
fatherIndex = (currentIndex - 1) / 2;
}
}
}
//将剩余的数构造成大根堆(通过顶端的数下降)
private static void heapify(int[] arr, int index, int size) {
int left=2*index+1;
int right=2*index+2;
int largestIndex;
while(left<size){
if(arr[left]<arr[right]&&right<size){
largestIndex=right;
}else {
largestIndex=left;
}
if(arr[index]>arr[largestIndex]){
break;
}
swap(arr,largestIndex,index);
index=largestIndex;
left=2*index+1;
right=2*index+2;
}
}
//交换数组中两个元素的值
public static void swap(int[] arr, int i, int j) {
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
}
冒泡
从头不停的比较,将最大的数换到尾端
//冒泡排序
int arr[]={9,8,7,6,5,4,3,2,1,0}; //10个数
for(int i=0;i<arr.length;i++){
for(int j=1;j<arr.length-i;j++){
if(arr[j-1]>arr[j]){
arr[j-1]=arr[j-1]+arr[j];
arr[j]=arr[j-1]-arr[j];
arr[j-1]=arr[j-1]-arr[j];
}
}
}
System.out.println(Arrays.toString(arr));
快速排序(分治+递归)
//分治+递归
public static void main(String[] args) {
int arr[]={9,8,7,6,5,4,3,2,1,0}; //10个数
sort(arr,0,arr.length-1);
System.out.println(Arrays.toString(arr));
}
private static void sort(int[] arr, int low, int high) {
if(low<high){
int index=partition(arr,low,high);
sort(arr,index+1,high);
sort(arr,low,index-1);
}
}
private static int partition(int[] arr, int low, int high) {
int i=low;
int j=high;
int x=arr[low];//取第一个数为基准点
while(i<j){
while(i<j && arr[j]>x){ //在右侧寻找比基准点小的
j--;
}
if(i<j){ //将寻找到的 右侧比基准点小的 放在i的位置
arr[i]=arr[j];
}
while(i<j && arr[i]<=x){//在左侧寻找比基准点大的
i++;
}
if(i<j){
arr[j]=arr[i];//将寻找到的 左侧比基准点大的位置,放在j的位置
j--;
}
}
arr[i]=x; //现在的i位置大于(等于)左边小于(等于)右边
return i;//返回这个位置,之后分别对low,i-1 i-1,high进行递归
}
}
归并排序
归并排序是经典的分治策略,它将问题分成(divide)一些小的问题然后递归求解,而治(conquer)的阶段则将分的阶段解得的各答案修补在一起
---------数据结构与算法分析 Java语言描述
如果数组长度为1,那么只有一个元素需要排序,答案是显然的。否则,递归的将前半部分进行排序,和将后半部分进行排序,得到排序后的两部分数据(递归最底层直接比较大小就行了),然后使用上面描述的合并算法将这两部分合到一起。
package suanfa;
import java.util.Arrays;
class Solution {
public static void main(String[] args) {
Solution solution = new Solution();
int[] nums = new int[]{4,3,5,2,1};
int[] temp = new int[nums.length];
solution.mergeSort(nums,0,4,temp);
for(int n:nums){
System.out.print(n + " ");
}
}
void mergeSort(int[] nums,int low,int high,int[] temp){
if(low == high) return;
else{
int mid = (low + high) / 2;
mergeSort(nums,low,mid,temp);
mergeSort(nums,mid + 1,high,temp);
merge(nums,low,mid,high,temp);
}
}
private void merge(int[] nums, int low, int mid, int high,int[] temp) {
int i = low,j = mid + 1;
int m = mid,n = high;
int k = 0;
while(i <= m && j <= n){
if(nums[i] < nums[j]) temp[k++] = nums[i++];
else temp[k++] = nums[j++];
}
while(i <= m){
temp[k++] = nums[i++];
}
while(j <= n){
temp[k++] = nums[j++];
}
for(int x = low;x <= high;x++){
nums[x] = temp[x - low];
}
}
}
基数排序
(小技巧:数字+空串的length算出位数)
class Solution {
public static void main(String[] args) {
Solution solution = new Solution();
int[] nums = {7,4,9,3,2,1,8,6,5};
solution.radixSort(nums);
for(int r:nums){
System.out.print(r + " ");
}
}
void radixSort(int[] nums){
//创建桶
int[][] bucket = new int[10][nums.length];
//记录桶中个数
int[] count = new int[10];
//计算位数,找最大值即可
int max = Integer.MIN_VALUE;
for(int i = 0;i < nums.length;i++){
if(max < nums[i]) max = nums[i];
}
int radisLength = (max + "").length();
for(int i = 0,n = 1;i < radisLength;i++,n*=10){
//按照第i位放入桶中
for(int j = 0;j < nums.length;j++){
//求第i位的数字 比如234 求百位234%1000/100 = 2 求十位234%100/10=3 求个位234%10/1=4
int index = nums[j] % (n*10) / n;
System.out.println(index);
//放入响应的桶中
bucket[index][count[index]++] = nums[j];
}
//从桶中取出放回arr
int index = 0;
for(int j = 0;j < count.length;j++){
for(int k = 0;k < count[j];k++){
nums[index++] = bucket[j][k];
}
count[j] = 0;
}
}
}
}
计数排序
每个数在一个数组中记录出现次数,这个数组首部代表最小值,尾部代表最大值。所以在这个数组中是按序存放的。最后遍历这个数组即可(注意:数组中只是存放数字出现的次数,数组元素本身的位置代表数字的大小)。
class Solution {
public static void main(String[] args) {
Solution solution = new Solution();
int[] nums = {7,4,9,3,2,1,8,6,5};
solution.countSort(nums);
for(int r:nums){
System.out.print(r + " ");
}
}
void countSort(int[] nums){
int min = Integer.MAX_VALUE;
int max = Integer.MIN_VALUE;
//找出数组最大最小值
for(int i = 0;i < nums.length;i++){
if(nums[i] < min) min = nums[i];
if(nums[i] > max) max = nums[i];
}
int[] figure = new int[max-min+1];
for(int i = 0;i < nums.length;i++){
figure[nums[i] - min]++;
}
int begin = 0;
for(int i = 0;i < figure.length;i++){
if(figure[i] != 0){
for(int j = 0;j < figure[i];j++){
nums[begin++] = min + i;
}
}
}
}
}
桶排序
分为多个桶,桶内以其他排序方式进行排序,然后对桶遍历即可。桶排序是计数 排序的一般化形式
class Solution {
public static void main(String[] args) {
Solution solution = new Solution();
int[] nums = {7,4,9,3,2,1,8,6,5};
solution.bucketSort(nums, 3);
for(int n:nums){
System.out.print(n + " ");
}
}
void bucketSort(int[] nums,int sum){ //sum为桶的数量
int min = Integer.MAX_VALUE;
int max = Integer.MIN_VALUE;
//找出数组最大最小值
for(int i = 0;i < nums.length;i++){
if(nums[i] < min) min = nums[i];
if(nums[i] > max) max = nums[i];
}
int[][] buckets = new int[sum][nums.length];
int[] c = new int[sum];
int count = (int)Math.ceil((nums.length * 1.0 / sum));
for(int i = 0;i < nums.length;i++){
int index = (nums[i]-min) / count;
buckets[index][c[index]++] = nums[i];
}
int k = 0;
for(int i = 0;i < sum;i++){
Arrays.sort(buckets[i], 0, c[i]);
for(int j = 0;j < c[i];j++){
nums[k++] = buckets[i][j];
}
}
}
}
查找
线性查找
示例为查找在数组中出现的第一个指定元素,用for循环即可找出,此之谓线性
public class Search {
public static void main(String[] args) {
int[] arr={53,3,542,748,14,214};
int index=seqSearch(arr,3);
if(index == -1){
System.out.println("没有找到该元素");
}else{
System.out.println("该元素在数组中第一次的索引为:"+index);
}
}
private static int seqSearch(int[] arr, int param) {
int index=-1;
for(int i=0;i<arr.length;i++){
if(arr[i] == param){
index=i;
}
}
return index;
}
}
因为是线性的,复杂度是O(n)
二分/折半查找
一般实现
二分查找的前提是数组必须有序
要寻找的值为findVal
left初始为0 , right初始为arr.length-1
mid=(left+right)/2
递归分三路
findVal>arr[mid] 往下递归 left=mid+1
findVal<arr[mid] 往下递归 right=mid-1
findVal == arr[mid] 开始return
递归结束的条件是 找到值return 或者 找不到left>right代表查找结束仍未找到,返回-1
public class Search {
public static void main(String[] args) {
int[] arr={1,3,5,7,9,11};
int index=binarySearch(arr,0,arr.length-1,5);
if(index == -1){
System.out.println("没有找到该元素");
}else{
System.out.println("该元素在数组中第一次的索引为:"+index);
}
}
private static int binarySearch(int[] arr, int left,int right,int findVal) {
if(left>right){
return -1;
}
int mid=(left+right)/2;
if(findVal > arr[mid]){
return binarySearch(arr,mid+1,right,findVal);
}else if(findVal < arr[mid]){
return binarySearch(arr,left,mid-1,findVal);
}else{
return mid;
}
}
}
因为是折半,所以效率很高,复杂度是对数级O(logn)
改进实现(所有相同元素索引)
当递归到与mid==findVal时,找出mid左右所有相同元素索引,然后返回,找不到返回空。
public class Search {
public static void main(String[] args) {
int[] arr={1,3,5,5,7,9,11};
ArrayList<Integer> binarySearch = binarySearch(arr,0,arr.length-1,5);
if(binarySearch == null){
System.out.println("没有找到该元素");
}else{
System.out.println("该元素在数组中第一次的索引为:"+binarySearch.toString());
}
}
private static ArrayList<Integer> binarySearch(int[] arr, int left,int right,int findVal) {
if(left>right){
return null;
}
int mid=(left+right)/2;
if(findVal > arr[mid]){
return binarySearch(arr,mid+1,right,findVal);
}else if(findVal < arr[mid]){
return binarySearch(arr,left,mid-1,findVal);
}else{
ArrayList<Integer>list=new ArrayList<>();
list.add(mid);
int temp=mid-1;
while(true){
//向左扫描
if(temp<0 || arr[temp]!=findVal){
break;
}
list.add(temp);
temp--;
}
int temp2=mid+1;
while(true){
//向右扫描
if(temp2>arr.length-1 || arr[temp2]!=findVal){
break;
}
list.add(temp2);
temp2++;
}
return list;
}
}
}
插值查找
插值查找是在二分查找的基础上,仅仅改进mid计算的方式,针对数据量大且分布比较均匀的数组要比折半效率高,如果分布不均匀则不一定
int mid=left+(right-left)*(findVal-arr[left])/(arr[right]-arr[left]);
斐波那契查找
斐波那契其实就是在二分查找的基础上改变了mid的选择方式
图解
package suanfa;
import java.util.Arrays;
public class Search {
public static void main(String[] args) {
int[] arr={1,3,5,5,7,9,11,13,15};
int fibonacciSearch = fibonacciSearch(arr,9);
if(fibonacciSearch == -1){
System.out.println("没有找到该元素");
}else{
System.out.println("该元素在数组中第一次的索引为:"+fibonacciSearch);
}
}
//得到一个斐波那契数列
public static int[] fib(){
int[] f=new int[20];
f[0]=1;
f[1]=1;
for(int i=2;i<20;i++){
f[i]=f[i-1]+f[i-2];
}
return f;
}
//非递归方式
private static int fibonacciSearch(int[] a,int key) {
int low=0;
int high=a.length-1;
int k=0; //表示斐波那契分割数值的下标
int mid=0;
int f[]=fib();//获取斐波那契数列
//获取到斐波那契分割数值的下标
while(a.length > f[k]-1){
k++;
}
//因为f[k]值可能大于a的长度,因此我们需要使用Arrays类构建一个新的数组
int[]temp=Arrays.copyOf(a, f[k]);
while(low<=high){ //该条件满足就可以找
for(int i=high+1;i<low+f[k];i++){
temp[i]=a[high];
}
mid = low + f[k-1] -1;
if(key < temp[mid]){
high=mid-1;
k--;
}else if(key > temp[mid]){
low=mid+1;
k-=2;
}else{
if(mid <= high){
return mid;
}else{
return high;
}
}
}
return -1;
}
}
哈希表
hashmap底层原理
每个对象有一个特殊的HashCode,它的范围是int的范围,对于HashMap,不同的算法有不同的存储方式。
1.如果不同的HashCode算出的值相同,则放到同一个链表中,按 顺序排放,例如求余数,肯定有余数相同的HashCode,余数相同的就放在同一个链表里面。高效的算法保证程序的高效性
2.如果极端化,用两种最愚蠢的算法,第一种,将数组的范围定义为和int一样,那么每个HashCode就对应一个数组元素,那么就不存在链表,就是一个数组;第二种,如果将运算的结果设定为同一个值,比如乘以0,那么就没有数组,只是一个链表,高效的算法就要找到分配最均匀的方式,远离这两个极端。
3.get方法的原理跟存储原理类似,根据HashCode确定对象所在的链表,然后调用equals方法一个个进行对比,如果有一个为true就返回这个里面的value。
4.内容相同的对象必须有相等的HashCode,相同hashcode不一定key相同
PS:JDK8之后,当链表长度大于8时,就转换为红黑树,又大大提高了查找效率。
手写hashMap核心方法
主要思路:
建立一个节点数组,也就是一个对象数组,里面装节点,然后每个节点作为链表的头建立一个链表。
我这里是通过取余计算Hash的值,HashCode%table.Length()
Hash作为数组的下标索引进行存储
Node[]table;
int size;
YYHashMap(){
table=new Node[16];
}
定义节点
package Text;
public class Node {
Node next;
Object value;
Object key;
public Node(Object key,Object value ) {
this.value = value;
this.key = key;
}
}
整个实现的核心是put方法
public void put(K key,V value){
int hash=key.hashCode()%(table.length);
Node newNode=new Node(key,value);
if(table[hash]==null){ //key对应的链表为空,就直接放入
table[hash]=newNode;
size++;
}
else{
Node temp= table[hash];
boolean a=false;
while(temp!=null){ //遍历链表如果有key重复,则覆盖value,用Boolean a 进行判断
if(temp.key.equals(newNode.key)){
temp.value=newNode.value;
a=true;
break;
}
temp = temp.next;
}
if(!a){ //如果没有重复
temp=getNode(table[hash]); //这里要注意上面遍历之后的temp是空的,所以不能直接拿来用,xie一个getCode方法获得节点
temp.next=newNode;
size++;
}
}
}
下面的完整代码
package Text;
public class Node {
Node next;
Object value;
Object key;
public Node(Object key,Object value ) {
this.value = value;
this.key = key;
}
}
package Text;
import java.security.Key;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
public class TextHashMap {
public static void main(String[] args) {
YYHashMap<Integer,String > map=new YYHashMap();
map.put(1,"张珊");
map.put(2,"李思");
map.put(3,"王物");
map.put(3,"Jenny");
map.put(1,"Tom");
System.out.println(map);
}
}
class YYHashMap<K,V>{
Node[]table;
int size;
YYHashMap(){
table=new Node[16];
}
public int hashCode(K key){
return key.hashCode();
}
public int size(){
return size;
}
public void put(K key,V value){
int hash=key.hashCode()%(table.length);
Node newNode=new Node(key,value);
if(table[hash]==null){ //key对应的链表为空,就直接放入
table[hash]=newNode;
size++;
}
else{
Node temp= table[hash];
boolean a=false;
while(temp!=null){ //遍历链表如果有key重复,则覆盖value,用Boolean a 进行判断
if(temp.key.equals(newNode.key)){
temp.value=newNode.value;
a=true;
break;
}
temp = temp.next;
}
if(!a){ //如果没有重复
temp=getNode(table[hash]); //这里要注意上面遍历之后的temp是空的,所以不能直接拿来用,xie一个getCode方法获得节点
temp.next=newNode;
size++;
}
}
}
private Node getNode(Node temp){
while(temp.next!=null){
temp=temp.next;
}
return temp;
}
public V get(K key){
int hashcode=key.hashCode();
int hash=hashcode%table.length;
Node temp=table[hash];
while(!temp.key.equals(key)){ //先算出hash值确定在数组中的位置,然后在链表中一一进行比较
if(temp.next==null){ //如果出现key相同的就返回value值,如果遍历完成仍然没有相同的就什么也不输出,返回一个空字符串
return (V) "";
}
}
return (V) temp.key;
}
public boolean isEmpty(){
for(int i=0;i<table.length;i++){
if(table[i]!=null){
return false;
}
}
return true;
}
@Override
public String toString() {
StringBuilder sb=new StringBuilder("{"); //重写toString方法,key=value形式输出
for(int i=0;i<table.length;i++){
Node temp=table[i]; //每次循环获得链表的头,遍历链表输出,toString都是老套路了
while(temp!=null){
sb.append(temp.key+"="+temp.value+",");
temp=temp.next;
}
}
sb.setCharAt(sb.length()-1,'}');
return sb.toString();
}
public void clear(){
for(int i=0;i<table.length;i++){
table[i]=null;
}
}
}
这个底层实现重在理解原理,当然还有很多复杂的算法没有实现,当然不能与JDK相提并论,但是通过这个可以对Map理解的比较深刻。
手写HashSet核心方法
先说使用方法吧,其实HashSet和ArrayList等等的使用方法都是基本一样的,测试代码献上.
```java
package Text;
import javax.xml.soap.Text;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Set;
import java.util.TreeSet;
public class TextHashSet {
public static void main(String[] args) {
Set<String> set1=new HashSet<>();
set1.add("aa");
set1.add("bb");
set1.add("cc");
set1.add("dd");
System.out.println(set1);
Set<String> set2=new HashSet<>();
set2.add("bb");
set2.add("cc");
set2.add("dd");
set2.add("ee");
System.out.println(set2);
// set1.addAll(set2);因为不允许重复,所以结果把相同的只保留一个
// set1.containsAll(set2); 返回Boolean类型,测试set1里面是否全部包含set2
// System.out.println(set1.containsAll(set2));判断set1是否包含全部set2
// System.out.println(set1.isEmpty());
// System.out.println(set1.remove("aa"));//这里是直接通过他的value进行删除
// set1.removeAll(set2);//把相同的都去掉,只set1保留不同的,set2没有改变
// set1.retainAll(set2);//只保留相同的在set1里面,set2不发生改变
// System.out.println(set1.size());
// System.out.println(set1.equals(set2));比较内容是否相同
}
}
set返回的是key的集合,所以可以直接用set进行for循环遍历,在后面的迭代器中将Map转化为Set就是因为Set遍历很方便。
public class TreeMap {
public static void main(String[] args) {
Set<Integer >set=new HashSet<>(); //底层实现是作为HashMap的key
set.add(1);
set.add(3);
set.add(2);
for (Integer temp:set //set本身返回的是key的集合
) {
System.out.println(temp);
}
}
}
下面这部分是手动实现核心功能,HashSet就是把内容作为HashMap的key进行存储,这也是为什么HashSet不能重复的原因。
package Text;
import javax.xml.soap.Text;
import java.util.HashMap;
import java.util.Set;
import java.util.TreeSet;
public class TextHashSet {
HashMap map;
private static final Object PRESENT=new Object();
public TextHashSet(){
map=new HashMap();
}
public void add(Object o){
map.put(o,PRESENT);
}
public int size(){
return map.size();
}
@Override
public String toString() {
StringBuilder sb=new StringBuilder();
sb.append("{");
for (Object key:map.keySet()
) {
sb.append(key+",");
}
sb.setCharAt(sb.length()-1, '}');
return sb.toString();
}
public static void main(String[] args) {
TextHashSet set=new TextHashSet();
set.add("AAA");
set.add("BBB");
set.add("CCC");
System.out.println(set);
}
}
树
1.基本概念
1.1 结点概念
结点是数据结构中的基础,是构成复杂数据结构的基本组成单位。
1.2 树结点声明
本系列文章中提及的结点专指树的结点。例如:结点A在图中表示为:
2.树
2.1 定义
**树(Tree)**是n(n>=0)个结点的有限集。n=0时称为空树。在任意一颗非空树中:
1)有且仅有一个特定的称为根(Root)的结点;
2)当n>1时,其余结点可分为m(m>0)个互不相交的有限集T1、T2、…、Tn,其中每一个集合本身又是一棵树,并且称为根的子树。
此外,树的定义还需要强调以下两点:
1)n>0时根结点是唯一的,不可能存在多个根结点,数据结构中的树只能有一个根结点。
2)m>0时,子树的个数没有限制,但它们一定是互不相交的。
示例树:
图2.1为一棵普通的树:
由树的定义可以看出,树的定义使用了递归的方式。递归在树的学习过程中起着重要作用,
2.2 结点的度
结点拥有的子树数目称为结点的度。
图2.2中标注了图2.1所示树的各个结点的度。
2.3 结点关系
结点子树的根结点为该结点的孩子结点。相应该结点称为孩子结点的双亲结点。
图2.2中,A为B的双亲结点,B为A的孩子结点。
同一个双亲结点的孩子结点之间互称兄弟结点。
图2.2中,结点B与结点C互为兄弟结点。
2.4 结点层次
从根开始定义起,根为第一层,根的孩子为第二层,以此类推。
图2.3表示了图2.1所示树的层次关系
2.5 树的深度
树中结点的最大层次数称为树的深度或高度。图2.1所示树的深度为4。
3 .二叉树
3.1 定义
二叉树是n(n>=0)个结点的有限集合,该集合或者为空集(称为空二叉树),或者由一个根结点和两棵互不相交的、分别称为根结点的左子树和右子树组成。
图3.1展示了一棵普通二叉树:
3.2 二叉树特点
由二叉树定义以及图示分析得出二叉树有以下特点:
1)每个结点最多有两颗子树,所以二叉树中不存在度大于2的结点。
2)左子树和右子树是有顺序的,次序不能任意颠倒。
3)即使树中某结点只有一棵子树,也要区分它是左子树还是右子树。
3.3 二叉树性质
1)在二叉树的第i层上最多有2i-1 个节点 。(i>=1)
2)二叉树中如果深度为k,那么最多有2k-1个节点。(k>=1)
3)n0=n2+1 n0表示度数为0的节点数,n2表示度数为2的节点数。
4)在完全二叉树中,具有n个节点的完全二叉树的深度为[log2n]+1,其中[log2n]是向下取整。
5)若对含 n 个结点的完全二叉树从上到下且从左至右进行 1 至 n 的编号,则对完全二叉树中任意一个编号为 i 的结点有如下特性:
(1) 若 i=1,则该结点是二叉树的根,无双亲, 否则,编号为 [i/2] 的结点为其双亲结点;
(2) 若 2i>n,则该结点无左孩子, 否则,编号为 2i 的结点为其左孩子结点;
(3) 若 2i+1>n,则该结点无右孩子结点, 否则,编号为2i+1 的结点为其右孩子结点。
3.4 斜树
斜树:所有的结点都只有左子树的二叉树叫左斜树。所有结点都是只有右子树的二叉树叫右斜树。这两者统称为斜树。
3.5 满二叉树
满二叉树:在一棵二叉树中。如果所有分支结点都存在左子树和右子树,并且所有叶子都在同一层上,这样的二叉树称为满二叉树。
满二叉树的特点有:
1)叶子只能出现在最下一层。出现在其它层就不可能达成平衡。
2)非叶子结点的度一定是2。
3)在同样深度的二叉树中,满二叉树的结点个数最多,叶子数最多。
3.6 完全二叉树
完全二叉树:对一颗具有n个结点的二叉树按层编号,如果编号为i(1<=i<=n)的结点与同样深度的满二叉树中编号为i的结点在二叉树中位置完全相同,则这棵二叉树称为完全二叉树。
图3.5展示一棵完全二叉树
特点:
1)叶子结点只能出现在最下层和次下层。
2)最下层的叶子结点集中在树的左部。
3)倒数第二层若存在叶子结点,一定在右部连续位置。
4)如果结点度为1,则该结点只有左孩子,即没有右子树。
5)同样结点数目的二叉树,完全二叉树深度最小。
注:满二叉树一定是完全二叉树,但反过来不一定成立。
3.7 二叉树的存储结构
3.7.1 顺序存储
二叉树的顺序存储结构就是使用一维数组存储二叉树中的结点,并且结点的存储位置,就是数组的下标索引。
图3.6
图3.6所示的一棵完全二叉树采用顺序存储方式,如图3.7表示:
由图3.7可以看出,当二叉树为完全二叉树时,结点数刚好填满数组。
那么当二叉树不为完全二叉树时,采用顺序存储形式如何呢?例如:对于图3.8描述的二叉树:
其中浅色结点表示结点不存在。那么图3.8所示的二叉树的顺序存储结构如图3.9所示:
其中,∧表示数组中此位置没有存储结点。此时可以发现,顺序存储结构中已经出现了空间浪费的情况。
那么对于图3.3所示的右斜树极端情况对应的顺序存储结构如图3.10所示:
由图3.10可以看出,对于这种右斜树极端情况,采用顺序存储的方式是十分浪费空间的。因此,顺序存储一般适用于完全二叉树。
3.7.2 二叉链表
既然顺序存储不能满足二叉树的存储需求,那么考虑采用链式存储。由二叉树定义可知,二叉树的每个结点最多有两个孩子。因此,可以将结点数据结构定义为一个数据和两个指针域。表示方式如图3.11所示:
定义结点代码:
public class Node {
Object value;
Node leftChild;
Node rightChild;
}
则图3.6所示的二叉树可以采用图3.12表示。
图3.12中采用一种链表结构存储二叉树,这种链表称为二叉链表。
4.树的遍历
package cn.wit;
public class Node {
Object value;
Node leftChild;
Node rightChild;
public Node( Node leftChild,Object value, Node rightChild) {
this.leftChild = leftChild;
this.value = value;
this.rightChild = rightChild;
}
public Node(Object value) {
this.value = value;
}
@Override
public String toString() {
return "Node{" +
"value=" + value +
", leftChild=" + leftChild +
", rightChild=" + rightChild +
'}';
}
}
package cn.wit;
public interface BinaryTree {
//判空
public boolean isEmpty();
//节点数量
public int size();
//树的深度
public int getHeight();
//通过值找节点
public Node findKey(Object value);
//递归遍历
public void preOrderTraverse();//先序遍历
public void inOrderTraverse();//中序遍历
public void postOrderTraverse();//后序遍历
//非递归遍历
public void inOrderByStack(); //中序遍历
public void preOrderByStack(); //先序遍历
public void postOrderByStack(); //后序遍历
//层序遍历
public void levelOrderByStack();
}
package cn.wit;
import java.util.Deque;
import java.util.LinkedList;
import java.util.Queue;
class LinkedBinaryTree implements BinaryTree{
public static void main (String[] args){
// 1
// 2 3
// 4 5 6 7
Node node4=new Node(4);
Node node5=new Node(5);
Node node6=new Node(6);
Node node7=new Node(7);
Node node2=new Node(node4,2,node5);
Node node3=new Node(node6,3,node7);
Node node1=new Node(node2,1,node3);
BinaryTree binaryTree = new LinkedBinaryTree(node1);
System.out.println(binaryTree.getHeight());
System.out.println(binaryTree.isEmpty());
System.out.println(binaryTree.size());
binaryTree.preOrderTraverse();
System.out.println();
binaryTree.inOrderTraverse();
System.out.println();
binaryTree.postOrderTraverse();
System.out.println();
binaryTree.preOrderByStack();
System.out.println();
binaryTree.inOrderByStack();
System.out.println();
binaryTree.postOrderByStack();
System.out.println();
binaryTree.levelOrderByStack();
}
private Node root;
public LinkedBinaryTree(Node root) {
this.root = root;
}
public LinkedBinaryTree() {
}
@Override
public boolean isEmpty() {
return false;
}
@Override
public int size() {
return size(root);
}
public int size(Node node) {
if(node == null){
return 0;
}else{
int l = this.size(node.leftChild);
int r = this.size(node.rightChild);
return l+r+1;
}
}
@Override
public int getHeight() {
return getHeight(root);
}
public int getHeight(Node node) {
if(node != null){
int l = getHeight(node.leftChild);
int r = getHeight(node.rightChild);
return l>r?(l+1):(r+1);
}
return 0;
}
@Override
public Node findKey(Object value) {
return findKey(value,root);
}
public Node findKey(Object value,Node node) {
if(node == null){
return null;
}else if(node != null && node.value == value){
return node;
}else{
Node lNode = findKey(value,node.leftChild);
Node rNode = findKey(value,node.rightChild);
if(lNode != null){
return lNode;
}else if(rNode != null){
return rNode;
}else{
return null;
}
}
}
//递归遍历
//先序递归
@Override
public void preOrderTraverse() {
preOrderTraverse(root);
}
public void preOrderTraverse(Node node) {
if(node != null){
System.out.print(node.value + " ");
preOrderTraverse(node.leftChild);
preOrderTraverse(node.rightChild);
}
}
//中序递归
@Override
public void inOrderTraverse() {
inOrderTraverse(root);
}
public void inOrderTraverse(Node node) {
if(node != null){
inOrderTraverse(node.leftChild);
System.out.print(node.value + " ");
inOrderTraverse(node.rightChild);
}
}
//后序递归
@Override
public void postOrderTraverse() {
postOrderTraverse(root);
}
public void postOrderTraverse(Node node) {
if(node != null){
postOrderTraverse(node.leftChild);
postOrderTraverse(node.rightChild);
System.out.print(node.value + " ");
}
}
@Override
public void preOrderByStack() {
if(root == null)return;
Deque<Node> de = new LinkedList<>();
de.push(root);
while(!de.isEmpty()){
Node temp = de.poll();
System.out.print(temp.value + " ");
if(temp.rightChild != null) de.push(temp.rightChild);
if(temp.leftChild != null) de.push(temp.leftChild);
}
}
// 1
// 2 3
// 4 5 6 7
//比如将根节点1最左侧所有节点压入栈中 124
//将4取出 打印4,将新的节点置为4的右节点,4没有右节点,2出栈,将新节点置为2的右节点5
//将5压入栈,没有左节点可以压入,5出栈打印,5没有右节点,
//1出栈并打印,压入3,压入6,6出栈并打印,3出栈并打印,压入7,7出栈并打印
//4 2 5 1 6 3 7
@Override
public void inOrderByStack() {
Deque<Node> de = new LinkedList<>();
Node curNode=root;
while(!de.isEmpty() || curNode != null){
while(curNode != null){
de.push(curNode);
curNode=curNode.leftChild;
}
Node temp = de.poll();
System.out.print(temp.value + " ");
curNode = temp.rightChild;
}
}
// 1
// 2 3
// 4 5 6 7
//将1放入d1
//取出d1,放入2,3 d2 1
//取出3,放入6,7 d2 1 3
//取出7 d2 1 3 7
//取出6 d2 1 3 7 6
//取出2,放入4,5 d2 1 3 7 6 2
//取出5 d2 1 3 7 6 2 5
//取出4 d2 1 3 7 6 2 5 4
//d2全部出栈 4 5 2 6 7 3 1
@Override
public void postOrderByStack() {
Deque<Node> d1 = new LinkedList<>();
Deque<Node> d2 = new LinkedList<>();
d1.push(root);
while(!d1.isEmpty()){
Node temp = d1.poll();
d2.push(temp);
if(temp.leftChild != null)d1.push(temp.leftChild);
if(temp.rightChild != null)d1.push(temp.rightChild);
}
while(!d2.isEmpty()){
Node temp=d2.poll();
System.out.print(temp.value + " ");
}
}
@Override
public void levelOrderByStack() {
Queue<Node> de=new LinkedList<>();
de.add(root);
while(de.size() != 0){
int len=de.size();
for(int i = 0;i < len;i++){
Node temp = de.poll();
System.out.print(temp.value + " ");
if(temp.leftChild != null)de.add(temp.leftChild);
if(temp.leftChild != null)de.add(temp.rightChild);
}
}
}
}
5.哈夫曼树
5.1赫夫曼编码-数据压缩原理
摘自百度百科(很多博客讲了半天都没讲清楚原理,不如百度百科):
哈夫曼编码是上个世纪五十年代由哈夫曼教授研制开发的,它借助了数据结构当中的树型结构,在哈夫曼算法的支持下构造出一棵最优二叉树,我们把这类树命名为哈夫曼树.因此,准确地说,哈夫曼编码是在哈夫曼树的基础之上构造出来的一种编码形式,它的本身有着非常广泛的应用.那么,哈夫曼编码是如何来实现数据的压缩和解压缩的呢?
众所周知,在计算机当中,数据的存储和加工都是以字节作为基本单位的,一个西文字符要通过一个字节来表达,而一个汉字就要用两个字节,我们把这种每一个字符都通过相同的字节数来表达的编码形式称为定长编码.以西文为例,例如我们要在计算机当中存储这样的一句话:I am a teacher.就需要15个字节,也就是120个二进制位的数据来实现.与这种定长编码不同的是,哈夫曼编码是一种变长编码.它根据字符出现的概率来构造平均长度最短的编码.换句话说如果一个字符在一段文档当中出现的次数多,它的编码就相应的短,如果一个字符在一段文档当中出现的次数少,它的编码就相应的长.当编码中,各码字的长度严格按照对应符号出现的概率大小进行逆序排列时,则编码的平均长度是最小的.这就是哈夫曼编码实现数据压缩的基本原理.要想得到一段数据的哈夫曼编码,需要用到三个步骤:第一步:扫描需编码的数据,统计原数据中各字符出现的概率.第二步:利用得到的概率值创建哈夫曼树.第三步:对哈夫曼树进行编码,并把编码后得到的码字存储起来.
因为定长编码已经用相同的位数这个条件保证了任一个字符的编码都不会成为其它编码的前缀,所以这种情况只会出现在变长编码当中,要想避免这种情况,我们就必须用一个条件来制约定长编码,这个条件就是要想成为压缩编码,变长编码就必须是前缀编码.什么是前缀编码呢?所谓的前缀编码就是任何一个字符的编码都不能是另一个字符编码的前缀.
那么哈夫曼编码是否是前缀编码呢?观察a、b、c、d构成的编码树,可以发现b之所以成为c的前缀,是因为在这棵树上,b成为了c的父结点,从在哈夫曼树当中,原文档中的数据字符全都分布在这棵哈夫曼树的叶子位置,从而保证了哈夫曼编码当中的任何一个字符的编码都不能是另一个字符编码的前缀.也就是说哈夫曼编码是一种前缀编码,也就保证了解压缩过程当中译码的准确性.哈夫曼编码的解压缩过程也相对简单,就是将编码严格按照哈夫曼树进行翻译就可以了,例如遇到000,就可以顺着哈夫曼树找到I,遇到101就可以顺着哈夫曼树找到空格,以此类推,我们就可以很顺利的找到原来所有的字符.哈夫曼编码是一种一致性编码,有着非常广泛的应用,例如在JPEG文件中,就应用了哈夫曼编码来实现最后一步的压缩;在数字电视大力发展的今天,哈夫曼编码成为了视频信号的主要压缩方式.应当说,哈夫曼编码出现,结束了熵编码不能实现最短编码的历史,也使哈夫曼编码成为一种非常重要的无损编码. [2]
静态哈夫曼方法的最大缺点就是它需要对原始数据进行两遍扫描:第一遍统计原始数据中各字符出现的频率,利用得到的频率值创建哈夫曼树并将树的有关信息保存起来,便于解压时使用;第二遍则根据前面得到的哈夫曼树对原始数据进行编码,并将编码信息存储起来。这样如果用于网络通信中,将会引起较大的延时;对于文件压缩这样的应用场合,额外的磁盘访间将会降低该算法的数据压缩速度。
数据压缩的步骤:
1.第一遍扫描数据,统计原始数据(原始数据可能是图片)中各字符出现的频率(图片各像素出现的频率),一个频率对应一种字符(或像素)
2.创建哈夫曼树并将树的信息保存起来
3.根据哈夫曼树中频率对应的字符(或像素)对原始数据进行编码,并将编码信息存储起来
我们压缩数据的需求(对这种编码方式的期望)?
1.使用频率高的数据尽可能短,使用频率低的数据尽可能长(在数据上表现为WPL最小)
2.是前缀编码
哈夫曼编码是如何满足这两条需求的?
1.将小的频率尽量留在靠近树的底部
2.所有频率都是叶子节点表示,又因为树结构,所以肯定是前缀编码
5.2哈夫曼编码实现
哈夫曼树节点
//哈夫曼树节点
class Node implements Comparable<Node>{
int value; //权值
Node left; //左子结点
Node right; //右子结点
public Node(int value) {
this.value = value;
}
@Override
public int compareTo(Node o) {
//表示从小到大排序
return this.value - o.value;
}
}
用一个codeList表存储所有频率和哈夫曼编码的对应关系
//Code存储频率和哈夫曼编码的对应关系
public class Code {
int weight;
String code;
public Code(int weight, String code) {
this.weight = weight;
this.code = code;
}
}
public class HuffmanTree {
public static void main(String[] args) {
//1.假设已经统计出原始数据的字符频率并保存到arr中 char[] chars={'a','b','c','d','e','f','h'};
int[] arr = {13,7,8,3,9,6,1};
Node root = createHuffmanTree(arr);
getHuffmanCode(root);
}
// 2.create HuffmanTree 并将树的信息保存起来
public static Node createHuffmanTree(int[] arr){
List<Node> nodes = new ArrayList<>();
for (int value : arr) {
nodes.add(new Node(value));
}
while (nodes.size() > 1){
//进行排序
Collections.sort(nodes);
//取出权值最小的结点
Node leftNode = nodes.get(0);
//取出权值第二小的结点
Node rightNode = nodes.get(1);
//合并生成一个新父结点
Node parent = new Node(leftNode.value + rightNode.value);
//将父结点左右子节点指向
parent.left = leftNode;
parent.right = rightNode;
//将用于合并的两个结点删除
nodes.remove(leftNode);
nodes.remove(rightNode);
//将新合并的结点添加
nodes.add(parent);
}
//返回霍夫曼树根节点
return nodes.get(0);
}
//3.计算树的编码
public static void getHuffmanCode(Node node){
//stack保存递归到叶子节点时上面所有的标志数,stack在递归中唯一(需要仔细琢磨)
Stack<Integer> stack = new Stack();
//codeList碰到一个叶子节点就保存一个code,codeList在递归中唯一
List<Code> codeList=new ArrayList<>();
recHuffmanCode(node, stack,codeList);
for (Code c:codeList
) {
System.out.println(c.weight+" "+c.code);
}
}
private static void recHuffmanCode(Node node, Stack<Integer> stack,List<Code> codeList) {
if(node == null)return;
//当递归进行到叶子节点时,输出stack
if(node.left == null && node.right == null) {
String c="";
for (Object i:stack.toArray()
) {
c+=i;
}
codeList.add(new Code(node.value,c));
}
stack.add(0);
recHuffmanCode(node.left, stack,codeList); //在递归中,所有往左边走的情况都标0,回溯一个就pop了
stack.pop();
stack.add(1);
recHuffmanCode(node.right, stack,codeList); //在递归中,所有往右边走的情况都标1,回溯一个就pop了
stack.pop();
}
}
6.线索二叉树
what? 线索二叉树是在普通二叉树的基础上进行构造的,将普通二叉树中未使用的left或right节点利用起来(被利用起来的称为线索指针),再加上了leftIsThread和leftIsThread两个标记属性,将一次遍历的过程进行标记
why? 通过一次构造线索二叉树,遍历不再需要递归或栈模拟递归,使得树的遍历效率更高
public class Node {
Node leftChild;
Object value;
Node rightChild;
boolean leftIsThread; //做孩子是否为线索
boolean rightIsThread; //右孩子是否为线索
public Node(Node leftChild,Object value, Node rightChild) {
this.leftChild = leftChild;
this.value = value;
this.rightChild = rightChild;
}
public Node(Object value) {
this.value = value;
}
}
6.1 中序线索
从普通二叉树到中序线索二叉树的标记规则:(下面pre表示遍历的上一个节点)
leftIsThread rightIsThread默认为false(boolean初始化就为false)
节点的左节点为空,leftIsThread 设置为true,leftChild = pre
节点上一个节点右节点为空,pre.rightIsThread 设置 true ,pre.rightChild=node
rightIsThread在这个过程中的标记作用:如果为true,表示右节点即为遍历的下一个;如果为false,表示右节点是有值的,对于中序遍历而言,需要找到其最左侧的leftChild为下一个遍历节点,这里通过leftIsTread为false判断是否是真实的左节点
public class ThreadTree{
private Node root; //根节点
private Node pre = null; //线索化的时候临时保存前驱
public ThreadTree(Node root){
this.root = root;
inThread(root);
}
//中序二叉树线索化
public void inThread(Node node){
if(node != null){
inThread(node.leftChild);
if(node.leftChild == null){ //当前节点 左节点为空,left指向前继节点
node.leftIsThread = true;
node.leftChild = pre;
}
if(pre != null && pre.rightChild == null){ //前继节点 右子节点为空 right指向当前节点
pre.rightIsThread = true;
pre.rightChild = node;
}
pre = node;
inThread(node.rightChild);
}
}
public void inThreadList(){
Node node=root;
if(node != null){
while(node != null && !node.leftIsThread){ //中序遍历,找到遍历的第一个
node = node.leftChild;
}
System.out.print(node.value);
while(node!=null){
if(node.rightIsThread){ //没有真实rightChild 直接输出下一个
node = node.rightChild;
}else{ //有真实rightChild 找到最左侧leftChild
node = node.rightChild;
while(node != null && !node.leftIsThread){ //中序遍历,找到遍历的第一个
node = node.leftChild;
}
}
if(node!=null){
System.out.print(","+node.value);
}
}
}
}
public static void main (String[] args){
// 1
// 2 3
// 4 5 6 7
// 线索化遍历gu 4(null,2) -> 2(4,5) -> 5(2,1) -> 1(2,3) -> 6(1,3) -> 3(6,7) -> 7(3,null)
//中序遍历结果 4 2 5 1 6 3 7
Node node4=new Node(4);
Node node5=new Node(5);
Node node6=new Node(6);
Node node7=new Node(7);
Node node2=new Node(node4,2,node5);
Node node3=new Node(node6,3,node7);
Node node1=new Node(node2,1,node3);
ThreadTree threadTree=new ThreadTree(node1);
threadTree.inThreadList();
}
}
6.2 前序线索
从普通二叉树到前序线索二叉树的标记规则:(下面pre表示遍历的上一个节点)
leftIsThread rightIsThread默认为false(boolean初始化就为false)
节点的左节点为空,leftIsThread 设置为true,leftChild = pre
节点上一个节点右节点为空,pre.rightIsThread 设置 true ,pre.rightChild=node
递归时不要递归leftIsThread为true的节点
public class ThreadTree{
private Node root; //根节点
private Node pre = null; //线索化的时候临时保存前驱
public ThreadTree(Node root){
this.root = root;
preThread(root);
}
//前序二叉树线索化
public void preThread(Node node){
if(node != null){
if(node.leftChild == null){ //当前节点 左节点为空,left指向前继节点
node.leftIsThread = true;
node.leftChild = pre;
}
if(pre != null && pre.rightChild == null){ //前继节点 右子节点为空 right指向当前节点
pre.rightIsThread = true;
pre.rightChild = node;
}
pre = node;
if(!node.leftIsThread){
preThread(node.leftChild);
}
preThread(node.rightChild);
}
}
public void preThreadList(){
Node node=root;
if(node != null){
System.out.print(node.value);
while(node!=null){
if(node.rightIsThread){ //没有真实rightChild 直接输出
node = node.rightChild;
}else{ //有真实rightChild
if(!node.leftIsThread){
node = node.leftChild;
}else{
node = node.rightChild;
}
}
if(node!=null){
System.out.print(","+node.value);
}
}
}
}
public static void main (String[] args){
// 1
// 2 3
// 4 5 6 7
// 1(2,3) -> 2(4,5) -> 4(2,5) -> 5(2,3) -> 3(6,7) -> 6(3,7) -> 7(3,null)
//前序遍历结果 1,2,4,5,3,6,7
Node node4=new Node(4);
Node node5=new Node(5);
Node node6=new Node(6);
Node node7=new Node(7);
Node node2=new Node(node4,2,node5);
Node node3=new Node(node6,3,node7);
Node node1=new Node(node2,1,node3);
ThreadTree threadTree=new ThreadTree(node1);
threadTree.preThreadList();
}
}
6.3 后序线索
后序构造线索有点不一样,pre无论有没有右节点都将它的右节点指向当前节点
从普通二叉树到后序线索二叉树的标记规则:(下面pre表示遍历的上一个节点)
leftIsThread rightIsThread默认为false(boolean初始化就为false)
节点的左节点为空,leftIsThread 设置为true,leftChild = pre
节点上一个节点 pre.rightIsThread 设置 true ,pre.rightChild=node
遍历时只有root节点rightIsThread为true,循环条件 加一个!node.leftIsThread,遇到root遍历结束
public class ThreadTree{
private Node root; //根节点
private Node pre = null; //线索化的时候临时保存前驱
public ThreadTree(Node root){
this.root = root;
preThread(root);
}
//后序二叉树线索化
public void preThread(Node node){
if(node != null){
preThread(node.leftChild);
preThread(node.rightChild);
if(node.leftChild == null){ //当前节点 左节点为空,left指向前继节点
node.leftIsThread = true;
node.leftChild = pre;
}
if(pre != null){ //前继节点 不论有没有右节点 right指向当前节点
pre.rightIsThread = true;
pre.rightChild = node;
}
pre = node;
}
}
public void preThreadList(){
Node node=root;
if(node != null){
while(node != null && !node.leftIsThread){ //找到最左侧leftChild作为遍历的第一个
node = node.leftChild;
}
System.out.print(node.value);
while(node!=null && node.rightIsThread){//有真实rightChild,只有root有真实的rightChild,退出循环
node = node.rightChild;
if(node!=null){
System.out.print(","+node.value);
}
}
}
}
public static void main (String[] args){
// 1
// 2 3
// 4 5 6 7
// 4(null,5) -> 5(4,2) -> 2(4,5) -> 6(2,7) -> 7(6,3) -> 3(6,7) -> 1(2,3)
//后序遍历结果 4,5,2,6,7,3,1
Node node4=new Node(4);
Node node5=new Node(5);
Node node6=new Node(6);
Node node7=new Node(7);
Node node2=new Node(node4,2,node5);
Node node3=new Node(node6,3,node7);
Node node1=new Node(node2,1,node3);
ThreadTree threadTree=new ThreadTree(node1);
threadTree.preThreadList();
}
}
7 排序二叉树 BST
what? 二叉搜索树是普通二叉树的一种特殊表现形式,特点是每个节点满足 大于左节点,小于右节点,中序遍历二叉搜索树,得到的是升序的结果。删除时需要获取父节点,我们在Node中添加parent属性
why? 二叉搜索树的搜索,插入,删除效率高
插入
如果root为空, root = new Node(x)
root不为空
…1 x大于当前节点值,如果右节点为空,则新建Node作为其右节点,并设置本节点为新节点的父节点,否则node=node.rightChild
…2 x小于当前节点值,如果左节点为空,则新建Node作为其左节点,并设置本节点为新节点的父节点,否则node=node.leftChild
搜索
递归搜索,如果x == node.value ,返回,找不到会返回空
删除
删除叶子节点,将parent的leftChild或rightChild设置为null
删除有左子节点的节点,将parent的leftChild或rightChild设置为该节点的leftChild
删除有右子节点的节点,将parent的leftChild或rightChild设置为该节点的rightChild
删除有左右节点的节点,找到右子树中的最小值节点(即右子树中一直找leftChild),将本节点的值换成该最小值,找到该最小值节点的父节点,将其leftChild或rightChild设置为null
public class Node {
Node leftChild;
int value;
Node rightChild;
Node parent;
public Node(Node leftChild,int value, Node rightChild) {
this.leftChild = leftChild;
this.value = value;
this.rightChild = rightChild;
}
public Node(int value) {
this.value = value;
}
}
public class BinarySortTree {
private Node root;
public BinarySortTree(Node root) {
this.root = root;
}
public BinarySortTree() {
this.root = null;
}
public Node search(int x){
return search(root,x);
}
public Node search(Node node,int x){
if(node == null || x == node.value)return node;
return x < node.value ? search(node.leftChild,x) : search(node.rightChild,x);
}
public Node insertIntoBST(int x) {
if(root == null) return root = new Node(x);
Node node = root;
while(node != null){
if(x > node.value){
if(node.rightChild == null){
Node temp = new Node(x);
node.rightChild = temp;
temp.parent = node;
return root;
}else node=node.rightChild;
}else if(x < node.value){
if(node.leftChild == null){
Node temp = new Node(x);
node.leftChild = temp;
temp.parent = node;
return root;
}else node =node.leftChild;
}
}
return root;
}
public Node deleteBSTNode(int x){
deleteBSTNode(root,x);
return root;
}
public void deleteBSTNode(Node node, int x) {
if(node == null)return;
if(node.value == x){
if(node.leftChild == null && node.rightChild == null){
//找到父节点
Node parent = node.parent;
//将父节点左节点或右节点设置为null
if(parent.leftChild == node) parent.leftChild = null;
else parent.rightChild = null;
}else if(node.leftChild != null && node.rightChild == null){ //左子树或右子树其中一个为空,另外一个不为空
//获取父节点
Node parent = node.parent;
//将父节点左节点或右节点设置为本节点的左节点
if(parent.leftChild == node) parent.leftChild = node.leftChild;
else parent.rightChild = node.leftChild;
}else if(node.rightChild != null && node.leftChild == null){
//获取父节点
Node parent = node.parent;
//将父节点左节点或右节点设置为本节点的右节点
if(parent.leftChild == node) parent.leftChild = node.rightChild;
else parent.rightChild = node.rightChild;
}else{
Node min = min(node);
node.value = min.value;
//获取min的父节点
Node parent = min.parent;
//将min父节点的左节点或右节点设置为null
if(parent.leftChild == min) parent.leftChild = null;
else parent.rightChild = null;
}
}else if(node.value < x){ //比节点值大,找右边子树
deleteBSTNode(node.rightChild,x);
}else{ //比节点值小,找左边子树
deleteBSTNode(node.leftChild,x);
}
}
/*
One step right and then always left
*/
public Node min(Node node) {
node = node.rightChild;
while (node.leftChild != null) node = node.leftChild;
return node;
}
public static void main(String[] args) {
// 4
// 2 5
// 1 3 6 7
// 8
BinarySortTree binarySortTree = new BinarySortTree();
//插入
binarySortTree.insertIntoBST(4);
binarySortTree.insertIntoBST(2);
binarySortTree.insertIntoBST(5);
binarySortTree.insertIntoBST(1);
binarySortTree.insertIntoBST(3);
binarySortTree.insertIntoBST(6);
binarySortTree.insertIntoBST(7);
binarySortTree.insertIntoBST(8);
//中序遍历升序输出
binarySortTree.inOrderTraverse();
//查找
System.out.println();
System.out.println(binarySortTree.search(4).value);
//删除
binarySortTree.deleteBSTNode(3);
//中序遍历升序输出
binarySortTree.inOrderTraverse();
}
//中序递归
public void inOrderTraverse() {
inOrderTraverse(root);
}
public void inOrderTraverse(Node node) {
if(node != null){
inOrderTraverse(node.leftChild);
System.out.print(node.value + " ");
inOrderTraverse(node.rightChild);
}
}
}
8 高度平衡的二叉搜索树
前言
对于高度平衡的二叉搜索树,实际上是进阶的二叉搜索树,二叉搜索树在插入删除搜索时时间复杂度为logn~n(最坏情况为一条链,时间复杂度n),高度平衡的二叉搜索树通过平衡二叉树的左右子树高度,使得最坏时间复杂度也为logn
尽管有许多不同的方式可以实现高度平衡的二叉搜索树,但他们有着共同的目标:
应该高度平衡属性
应该支持二叉搜索树的基本操作,最坏时间复杂度在longn内的插入删除查询
在这里只讨论AVL树和红黑树,该数据结构的具体实现考察较少,本篇不讨论具体实现
8.1 AVL树
特点:每次插入或删除,平衡左子树和右子树,使他们相差一个层级以内
8.2 红黑树
Java中TreeSet和TreeMap均是用的红黑树,HashMap中当链表超过8个节点,也会采用红黑树的方式进行存储,提高查找效率
红黑树是平衡二叉搜索树
根节点是黑色
红色节点的非空子节点必须是黑色,反之同理
空的叶子结点是黑色
B树
为什么要使用 B树呢?
要解释清楚这一点,我们假设我们的数据量达到了亿级别,主存当中根本存储不下,我们只能以块的形式从磁盘读取数据,与主存的访问时间相比,磁盘的 I/O 操作相当耗时,而提出 B树的主要目的就是减少磁盘的 I/O 操作。
B-
特点:多叉,降低树的深度
小于17的放p1,
大于17小于35放p2,
大于35放p3.
下面的然后再细分
B+
相比于B的特点就是所有的值全都放在最下面
5-28放p1
28-65放p2
65+放p3
B*
特点:在B+的基础上有指向兄弟的指针
图
前言
基本定义
存储方式
顺序存储
链式存储
图的遍历
图的遍历一般分为图的深度优先遍历 DFS(Depth First Search) 和图的广度优先遍历 BFS(Breadth First Search)
二叉树的前序、中序、后序遍历本质上都是深度优先遍历
二叉树的层序遍历本质上是广度优先遍历
遍历过程
深度优先遍历顺序:ABCFDHIEG
广度优先遍历:ABCDEFGHI
图结构实现
测试是依照上面遍历过程中的图片进行构造的
package test;
import java.util.ArrayList;
import java.util.Arrays;
public class Graph {
private ArrayList<String> vertexList; //图中定点集合
private int[][] edges; //存储图对应的领接矩阵
private int numOfEdges; //表示边的个数
//定义标记布尔数组boolean[] 记录对应节点是否被访问
private boolean[] isVisited;
//构造器
public Graph(int n){
//初始化矩阵和vertexList
edges = new int[n][n];
vertexList = new ArrayList<String>(n);
isVisited = new boolean[n];
}
//插入定点
public void insertVertex(String vertex){
vertexList.add(vertex);
}
//添加边
public void insertEdge(int v1,int v2,int weight){
edges[v1][v2] = weight;
edges[v2][v1] = weight;
numOfEdges++;
}
//返回节点个数
public int getVertexNumber(){
return vertexList.size();
}
//返回边的数目
public int getEdgesNumber(){
return numOfEdges;
}
//返回节点对应的数据
public String getValueByIndex(int i){
return vertexList.get(i);
}
//返回v1和v2的权值
public int getWeight(int v1,int v2){
return edges[v1][v2];
}
//显示图矩阵
public void showGraph(){
for(int[] link : edges){
System.out.println(Arrays.toString(link));
}
}
public static void main(String[] args) {
//节点为9的
int n=9;
String[] vertexs = {"A","B","C","D","E","F","G","H","I"};
Graph graph = new Graph(9);
for(String vertex :vertexs){
graph.insertVertex(vertex);
}
// A-0 B-1 C-2 D-3 E-4 F-5 G-6 H-7 I-8
graph.insertEdge(0, 1, 1); //AB
graph.insertEdge(0, 2, 1); //AC
graph.insertEdge(0, 3, 1); //AD
graph.insertEdge(1, 4, 1); //BE
graph.insertEdge(1, 2, 1); //BC
graph.insertEdge(4, 6, 1); //EG
graph.insertEdge(2, 5, 1); //CD
graph.insertEdge(3, 5, 1); //DF
graph.insertEdge(5, 7, 1); //FH
graph.insertEdge(7, 8, 1); //HI
graph.showGraph();
}
}
DFS实现
通用方法
/**
*
* 获取第一个未遍历邻接节点的下标
* @return 如果存在就返回对应的下标,没找到返回-1
*/
public int getFirstNeighbor(int index){
//遍历edges
for(int j = 0;j < vertexList.size();j++){
if(edges[index][j] > 0 && !isVisited[j]){
return j;
}
}
return -1;
}
递归
//dfs
//这是一个递归方法,第一次调用是A节点
public void dfs(int i){
//访问该节点
System.out.print(getValueByIndex(i) + "->");
//将该节点设置已访问
isVisited[i] = true;
//查找当前节点的第一个邻接节点
int w = getFirstNeighbor(i);
if(w != -1){
dfs(w);
w = getFirstNeighbor(i);
while(w != -1){
dfs(w);
w = getFirstNeighbor(i);
}
}
}
非递归
第一个push入栈
pop出栈,将第一个的所有邻接节点push入栈
栈顶pop出栈,将该节点的所有邻接节点push入栈
…
在这个过程中,当已经走到底时,开始模拟递归的回溯
//非递归栈实现
public void dfs(){
Deque<Integer> deque = new LinkedList<>();
deque.push(0);
isVisited[0] = true;
while(!deque.isEmpty()){
int k = deque.pop();
System.out.print(vertexList.get(k)+"->");
//将所有的邻接节点都压入栈(模拟递归)
int w = getFirstNeighbor(k);
while(w != -1){
deque.push(w);
isVisited[w] = true;
w = getFirstNeighbor(k);
}
}
}
BFS实现
队列实现
//bfs
//队列实现
public void bfs(){
Queue<Integer> queue = new LinkedList();
queue.offer(0);
isVisited[0] = true;
while(!queue.isEmpty()) {
int k = queue.remove();
System.out.print(vertexList.get(k)+"->");
//将所有的邻接节点都进队列
int w = getFirstNeighbor(k);
while (w != -1) {
queue.offer(w);
isVisited[w] = true;
w = getFirstNeighbor(k);
}
}
}
测试
package cn.wit;
public class Test {
public static void main(String[] args) {
//节点为9的
int n=9;
String[] vertexs = {"A","B","C","D","E","F","G","H","I"};
Graph graph = new Graph(9);
for(String vertex :vertexs){
graph.insertVertex(vertex);
}
// A-0 B-1 C-2 D-3 E-4 F-5 G-6 H-7 I-8
graph.insertEdge(0, 1, 1); //AB
graph.insertEdge(0, 2, 1); //AC
graph.insertEdge(0, 3, 1); //AD
graph.insertEdge(1, 4, 1); //BE
graph.insertEdge(1, 2, 1); //BC
graph.insertEdge(4, 6, 1); //EG
graph.insertEdge(2, 5, 1); //CD
graph.insertEdge(3, 5, 1); //DF
graph.insertEdge(5, 7, 1); //FH
graph.insertEdge(7, 8, 1); //HI
graph.showGraph();
//graph.dfs(0); //递归
//graph.dfs(); //栈模拟递归
graph.bfs();
}
}
prim
以图结构实现中为基础添加prim方法
修路问题
ABCDEFG七个村庄修路,抽象化为一张图,两村庄之间的距离为为两节点边的权值,求怎么修路长度最短
思路
- prim算法实际上是寻找图的最小生成树(MST)的过程
- 将已经选择的节点作为子图,在子图节点与非子图节点连接的所有边中寻找权值最小的边,找到后将该非子图节点标记为true,将其加入到子图中构成新的子图,继续根据新的子图寻找,这样寻找n-1次
实现
打印出修路的线路
public void prim() {
System.out.println("修路问题最短路径:");
isVisited[0] = true;
int h1 = -1;
int h2 = -1;
int minWeight = 10000;
for (int k = 1; k < vertexList.size(); k++) {
for (int i = 0; i < vertexList.size(); i++) {
for (int j = 0; j < vertexList.size(); j++) {
if (isVisited[i] && !isVisited[j] && (edges[i][j] < minWeight && edges[i][j] != 0)) {
minWeight = edges[i][j];
h1 = i;
h2 = j;
}
}
}
isVisited[h2] = true;
System.out.println(vertexList.get(h1) + "->"+vertexList.get(h2)+" "+edges[h1][h2]);
minWeight = 10000;
}
}
测试
package cn.wit;
public class Test3 {
public static void main(String[] args) {
Graph graph = new Graph(7);
String[] vertexs = {"A","B","C","D","E","F","G"};
for(String vertex :vertexs){
graph.insertVertex(vertex);
}
//0-A 1-B 2-C 3-D 4-E 5-F 6-G
graph.insertEdge(0, 1, 5); //AB
graph.insertEdge(0, 6, 2); //AG
graph.insertEdge(0, 2, 7); //AC
graph.insertEdge(1, 6, 3); //BG
graph.insertEdge(1, 3, 9); //BD
graph.insertEdge(2, 4, 8); //CE
graph.insertEdge(4, 5, 5); //EF
graph.insertEdge(5, 3, 4); //FD
graph.insertEdge(5, 6, 6); //FG
graph.insertEdge(4, 6, 4); //EG
graph.prim();
}
}
kruskal
思路
kruskal算法是另外一种寻找图的最小生产树的过程
按照权值从小到大选择边,每次选择的规则是保证该边与已经选择的边不能构成环
kruskal的核心就在于如何判断新加入的边与已有的生成子树是否构成环,构成环可以转换为两个节点是否连通的问题,就可以通过并查集来实现,getEnd方法就相当于并查集的find,循环体内代码 就相当于并查集的union,通过找到新加入边的两个节点在并查集中的根节点,如果不相同,说明不会成环(Quick-Union)
实现
class Edata implements Comparable<Edata> {
int start;//边的一个点
int end;//边的另外一个点
int weight;//边的权值
public Edata(int start, int end, int weight) {
super();
this.start = start;
this.end = end;
this.weight = weight;
}
@Override
public int compareTo(Edata o) {
// TODO Auto-generated method stub
return this.weight - o.weight;
}
@Override
public String toString() {
return "Edata{" +
"start=" + start +
", end=" + end +
", weight=" + weight +
'}';
}
}
public void kruskal(){
int index = 0;
//边对象数组
Edata[] edatas=new Edata[numOfEdges];
//装生成子树边对象
List<Edata> edataList = new ArrayList<>();
//记录生成子树中每个节点指向的另一个节点,为0表示暂时未在生成子树中
int ends[] = new int[numOfEdges];
//构建边对象数组
for(int i = 0;i < vertexList.size();i++){
for(int j = i+1;j < vertexList.size();j++){
if(edges[i][j] != 0){
Edata edata = new Edata(i,j,edges[i][j]);
edatas[index++] = edata;
}
}
}
//对边的权进行排序
Arrays.sort(edatas);
System.out.println(Arrays.toString(edatas));
//kruskal核心部分 加入边
for(int i = 0;i < numOfEdges; i++){
int p1 = edatas[i].start;
int p2 = edatas[i].end;
int m = getEnd(ends,p1);
int n = getEnd(ends,p2);
if(m != n){ //不构成回路,即没有相同的终点
ends[m] = n;
edataList.add(edatas[i]);
}
}
//输出最小生成树
for(Edata edata : edataList){
System.out.println(vertexList.get(edata.start) +"->" + vertexList.get(edata.end)+" "+edata.weight);
}
}
private int getEnd(int[] ends, int p1) {
while(ends[p1]!=0) {
p1=ends[p1];
}
return p1;
}
测试
public class Test3 {
public static void main(String[] args) {
Graph graph = new Graph(7);
String[] vertexs = {"A","B","C","D","E","F","G"};
for(String vertex :vertexs){
graph.insertVertex(vertex);
}
//0-A 1-B 2-C 3-D 4-E 5-F 6-G
graph.insertEdge(0, 1, 5); //AB
graph.insertEdge(0, 6, 2); //AG
graph.insertEdge(0, 2, 7); //AC
graph.insertEdge(1, 6, 3); //BG
graph.insertEdge(1, 3, 9); //BD
graph.insertEdge(2, 4, 8); //CE
graph.insertEdge(4, 5, 5); //EF
graph.insertEdge(5, 3, 4); //FD
graph.insertEdge(5, 6, 6); //FG
graph.insertEdge(4, 6, 4); //EG
//graph.prim();
graph.kruskal();
}
}
边遍历的顺序:
0 6
1 6
3 5
4 6
0 1
4 5
5 6
0 2
2 4
1 3
Dijkstra
前言
Dijkstra是一种用来求图中一个点到其他点最短路径的算法
思路
假定我们寻找G点到其他点的最短路径
设定三个数组
already_arr:对应节点是否被选择过
pre_visited:节点最短路径上一个节点索引,用来输出路径
dis:出发点到其他节点的距离,动态更新
初始化:
already_arr[start]设置为true,表示出发节点已访问
pre_Visited将出发点设置为一个较大值 65535,表示出发节点没有上个节点
Arrays.fill(dis,65535) 将节点初始化为65535表示初始无路径,dis[start] = 0 表示start节点到本身距离为0
核心部分:
大致思路:
先从起始节点开始,寻找一步能够到达的节点,更新dis和pre_visited(update完成),然后从这些更新过的节点中选择一个index(updateArr完成),遍历所有节点,判断出发节点到新index的距离+新index到遍历节点的距离 是否 小于原来出发节点到遍历节点的最短距离,如果小于,说明有了更短的路径,更新最短路径update:
遍历所有节点,len = dis[index] + edges[index][j],如果直接是连通的(edges[index][j]有正常值),更新dis和pre_visited
如起始节点G,dis[index] =0,ABEF都连通,则更新dis距离GA GB GE GF 路径,并且ABEF上一个节点更新为GupdateArr:
选择没有被选择过,并且要与起始节点有到达的路径,路径最短的节点
实现
package cn.wit;
import java.util.*;
public class Graph {
private ArrayList<String> vertexList; //图中定点集合
private int[][] edges; //存储图对应的领接矩阵
private int numOfEdges; //表示边的个数
//构造器
public Graph(int n) {
//初始化矩阵和vertexList
edges = new int[n][n];
vertexList = new ArrayList<String>(n);
for(int i = 0;i < n;i++){
Arrays.fill(edges[i],65535);
}
}
//插入定点
public void insertVertex(String vertex) {
vertexList.add(vertex);
}
//添加边
public void insertEdge(int v1, int v2, int weight) {
edges[v1][v2] = weight;
edges[v2][v1] = weight;
numOfEdges++;
}
//显示图矩阵
public void showGraph() {
for (int[] link : edges) {
System.out.println(Arrays.toString(link));
}
}
public void dijkstra(int index){
Dijkstra dijkstra = new Dijkstra(index,edges);
dijkstra.dsj();
}
}
public class Dijkstra {
//记录各个顶点是否访问过
public boolean[] already_arr;
//每个下标对应的值为前一个节点
public int[] pre_visited;
//记录出发顶点到其他顶点的距离
public int[] dis;
private int[][] edges; //存储图对应的领接矩阵
int start;
int length;
//length顶点个数,index出发顶点下标
public Dijkstra(int start , int[][] edges){
this.length = edges.length;
this.already_arr = new boolean[length];
this.pre_visited = new int[length];
this.dis = new int[length];
this.edges = edges;
this.start = start;
already_arr[start] = true;
pre_visited[start] = 65535;
Arrays.fill(dis,65535);
dis[start] = 0;
}
public void dsj(){
int index = start;
update(index);
for(int j = 1;j < length;j++){
index = updateArr();
update(index);
}
for(int i = 0;i < length;i++){
int k = i;
while(pre_visited[k] != 65535){
System.out.print((char)(k+65)+"->");
k = pre_visited[k];
}
System.out.println((char)(start+65)+" "+dis[i]);
}
}
private void update(int index){
int len = 0;
for(int j =0 ;j < length; j++){
len = dis[index] + edges[index][j];
if(len < dis[j]){
pre_visited[j] = index;
dis[j] = len;
}
}
}
public int updateArr(){
int index = 0;
int min = 65535;
for(int i = 0;i < length;i++){
if(!already_arr[i] && dis[i] < min){
index = i;
min = dis[i];
}
}
already_arr[index] = true;
return index;
}
}
测试
public class Test4 {
public static void main(String[] args) {
Graph graph = new Graph(7);
String[] vertexs = {"A","B","C","D","E","F","G"};
for(String vertex :vertexs){
graph.insertVertex(vertex);
}
//0-A 1-B 2-C 3-D 4-E 5-F 6-G
graph.insertEdge(0, 1, 5); //AB
graph.insertEdge(0, 6, 2); //AG
graph.insertEdge(0, 2, 7); //AC
graph.insertEdge(1, 6, 3); //BG
graph.insertEdge(1, 3, 9); //BD
graph.insertEdge(2, 4, 8); //CE
graph.insertEdge(4, 5, 5); //EF
graph.insertEdge(5, 3, 4); //FD
graph.insertEdge(5, 6, 6); //FG
graph.insertEdge(4, 6, 4); //EG
graph.dijkstra(6);
}
}
Floyd
前言
Floyd算法也是寻找最短路径的算法,dijkstra算法是寻找一个点到其他点的最短路径,Floyd算法是寻找每个节点到其他节点的最短路径,Floyd相比dijkstra时间复杂度较高
思路
pre 记录路径数组
dis 记录每个节点到其他节点最短距离(动态更新)
edges Graph传入,邻接矩阵数组
vertexList Graph传入,节点信息
初始化:
pre数组第i行全部是i,D->A节点,最短路径DFGA,pre[0][3]存放的是最短路径中D的下一个节点5-F,pre[0][5]存放F的下一个节点6-G,pre[0][6]的存放的值为0,碰到0结束(pre的特性体现在showGraph方法里面)
dis数组 dis[i][i]初始化为0,有权值的位置复制edges,其他为N
核心部分:
三个循环
第一层循环k,表示中间节点
第二层循环i,表示起始节点
第三层循环,表示终点
min(dis[i][j],dis[i][k]+dis[k][j])
如果更新dis[i][j],pre[i][j] = pre[k][j];
实现
public class Graph {
private ArrayList<String> vertexList; //图中定点集合
private int[][] edges; //存储图对应的领接矩阵
private int numOfEdges; //表示边的个数
//构造器
public Graph(int n) {
//初始化矩阵和vertexList
edges = new int[n][n];
vertexList = new ArrayList<String>(n);
for(int i = 0;i < n;i++){
Arrays.fill(edges[i],65535);
}
}
//插入定点
public void insertVertex(String vertex) {
vertexList.add(vertex);
}
//添加边
public void insertEdge(int v1, int v2, int weight) {
edges[v1][v2] = weight;
edges[v2][v1] = weight;
numOfEdges++;
}
public void floyd(){
Floyd floyd = new Floyd(edges,vertexList);
floyd.showGraph();
}
}
public class Floyd {
private int[][]pre;
private int[][]dis;
private int[][]edges;
private ArrayList<String> vertexList; //图中定点集合
int length;
public Floyd(int [][]edges,ArrayList<String> vertexList){
this.length = edges.length;
this.dis = new int[length][length];
this.pre = new int[length][length];
this.edges = edges;
this.vertexList = vertexList;
for(int i = 0;i < length;i++){
for(int j = 0;j < length;j++){
if(edges[i][j] == 0){
dis[i][j] = 65535;
}else{
dis[i][j] = edges[i][j];
}
if(i == j){
dis[i][j] = 0;
}
}
}
for(int i = 0;i < length;i++){
for(int j = 0;j < length;j++){
pre[i][j] = i;
}
}
floyd();
}
//显示图矩阵
public void showGraph() {
for (int[] link : dis) {
System.out.println(Arrays.toString(link));
}
System.out.println();
System.out.println();
for (int[] link : pre) {
System.out.println(Arrays.toString(link));
}
for(int i = 0;i < length;i++){
for(int j = 0;j < length;j++){
System.out.print(vertexList.get(j)+"->"+vertexList.get(i)+"路径:");
System.out.print(vertexList.get(j)+"->");
int a = pre[i][j];
while(true){
if(a == i){
break;
}
System.out.print(vertexList.get(a)+"->");
a = pre[i][a];
}
System.out.print(vertexList.get(i));
System.out.println(" "+dis[i][j]);
}
}
}
public void floyd(){
int len = 0;
//对中间顶点遍历,k就是中间顶点的下标
for(int k = 0;k < length;k++){
for(int i = 0;i < length;i++){
for(int j = 0;j < length;j++) {
len = dis[i][k] + dis[k][j];//求出从i顶点出发经过k到达j的距离
if(len < dis[i][j]){ //如果len小于dis[i][j],更新
dis[i][j] = len;
pre[i][j] = pre[k][j];
}
}
}
}
}
}
测试
public class Test4 {
public static void main(String[] args) {
Graph graph = new Graph(7);
String[] vertexs = {"A","B","C","D","E","F","G"};
for(String vertex :vertexs){
graph.insertVertex(vertex);
}
//0-A 1-B 2-C 3-D 4-E 5-F 6-G
graph.insertEdge(0, 1, 5); //AB
graph.insertEdge(0, 6, 2); //AG
graph.insertEdge(0, 2, 7); //AC
graph.insertEdge(1, 6, 3); //BG
graph.insertEdge(1, 3, 9); //BD
graph.insertEdge(2, 4, 8); //CE
graph.insertEdge(4, 5, 5); //EF
graph.insertEdge(5, 3, 4); //FD
graph.insertEdge(5, 6, 6); //FG
graph.insertEdge(4, 6, 4); //EG
graph.floyd();
}
}
[0, 5, 7, 12, 6, 8, 2]
[5, 0, 12, 9, 7, 9, 3]
[7, 12, 0, 17, 8, 13, 9]
[12, 9, 17, 0, 9, 4, 10]
[6, 7, 8, 9, 0, 5, 4]
[8, 9, 13, 4, 5, 0, 6]
[2, 3, 9, 10, 4, 6, 0]
[0, 0, 0, 5, 6, 6, 0]
[1, 1, 0, 1, 6, 6, 1]
[2, 0, 2, 5, 2, 4, 0]
[6, 3, 4, 3, 5, 3, 5]
[6, 6, 4, 5, 4, 4, 4]
[6, 6, 4, 5, 5, 5, 5]
[6, 6, 0, 5, 6, 6, 6]
A->A路径:A->A 0
B->A路径:B->A 5
C->A路径:C->A 7
D->A路径:D->F->G->A 12
E->A路径:E->G->A 6
F->A路径:F->G->A 8
G->A路径:G->A 2
A->B路径:A->B 5
B->B路径:B->B 0
C->B路径:C->A->B 12
D->B路径:D->B 9
E->B路径:E->G->B 7
F->B路径:F->G->B 9
G->B路径:G->B 3
A->C路径:A->C 7
B->C路径:B->A->C 12
C->C路径:C->C 0
D->C路径:D->F->E->C 17
E->C路径:E->C 8
F->C路径:F->E->C 13
G->C路径:G->A->C 9
A->D路径:A->G->F->D 12
B->D路径:B->D 9
C->D路径:C->E->F->D 17
D->D路径:D->D 0
E->D路径:E->F->D 9
F->D路径:F->D 4
G->D路径:G->F->D 10
A->E路径:A->G->E 6
B->E路径:B->G->E 7
C->E路径:C->E 8
D->E路径:D->F->E 9
E->E路径:E->E 0
F->E路径:F->E 5
G->E路径:G->E 4
A->F路径:A->G->F 8
B->F路径:B->G->F 9
C->F路径:C->E->F 13
D->F路径:D->F 4
E->F路径:E->F 5
F->F路径:F->F 0
G->F路径:G->F 6
A->G路径:A->G 2
B->G路径:B->G 3
C->G路径:C->A->G 9
D->G路径:D->F->G 10
E->G路径:E->G 4
F->G路径:F->G 6
G->G路径:G->G 0
并查集
前言
并查集一般用来判断两个节点是否连通 或 图中连通子图的数量
并查集中数组id[] ,该数组的索引映射着每个节点,每个索引对应的值为该节点 直接或间接 的根节点
如下图,有四种实现方式,它们的效率是递进的(也是一种逐步演化的过程):
count():组的个数,初始化组有N个(N为节点数),union后count–,属于通用方法
connected(int p, int q): p和q分别代表新加入边的两个节点,调用find方法判断是否连通,属于通用方法
union() 四种有不同实现
find() 四种有不同实现
Quick-Find
find(int p)
id数组直接记录每个节点的根节点(每个节点初始根节点为本身)
union(int p,int q)
pID qID分别为 p、q的根节点索引
find( p) 和find(q)如果指向同一根节点,则pq已连通,不用union
如果不是同一根节点,将所有根节点为pID的节点的根节点都更新为qID(将一个组的根节点全部指向另外一个组的根节点)
package cn.wit;
public class UF
{
private int[] id; // access to component id (site indexed)
private int count; // number of components
public UF(int N)
{
// Initialize component id array.
count = N;
id = new int[N];
for (int i = 0; i < N; i++)
id[i] = i;
}
public int count()
{ return count; }
public boolean connected(int p, int q)
{ return find(p) == find(q); }
public int find(int p)
{ return id[p]; }
public void union(int p, int q)
{
// 获得p和q的上司
int pID = find(p);
int qID = find(q);
// 如果两个组号相等,直接返回
if (pID == qID) return;
// 遍历一次,所有上司是pID的人员,他们的上司都变为qID完成合并
for (int i = 0; i < id.length; i++)
if (id[i] == pID) id[i] = qID;
count--;
}
}
Quick-Union
Quick-Find缺点:M条路径,N个节点,union时间复杂度 O(n^2)
Quick-Find时间复杂度高主要在于每个节点都单独保存其根节点
Quick-Union的id存储的不是根节点,而是它的上一个节点,这样find通过循环找到根节点(根节点具有性质id[root] = root)
Quick-Union的union方法是将p的根节点 指向 q节点(p对应的整个树成为q根节点的子树)
Quick-Union find方法查找树的高度O(n) union基于find,也是O(n)
package cn.wit;
public class UF
{
private int[] id; // access to component id (site indexed)
private int count; // number of components
public UF(int N)
{
// Initialize component id array.
count = N;
id = new int[N];
for (int i = 0; i < N; i++)
id[i] = i;
}
public int count()
{ return count; }
public boolean connected(int p, int q)
{ return find(p) == find(q); }
private int find(int p)
{
// 寻找p节点所在组的根节点,根节点具有性质id[root] = root
while (p != id[p]) p = id[p];
return p;
}
public void union(int p, int q)
{
// Give p and q the same root.
int pRoot = find(p);
int qRoot = find(q);
if (pRoot == qRoot)
return;
id[pRoot] = qRoot; // 将一颗树(即一个组)变成另外一课树(即一个组)的子树
count--;
}
}
Weighted Quick-Union
Quick-Union缺点:id[pRoot] = qRoot 针对树结构,这样的方式很可能会出现树结构退化成链表的情况
Weighted Quick-Union对比Quick-Union高度大大减小, find的时间复杂度O(lgn), union基于find,也是O(lgn)
从逻辑上分析如何避免这种退化情况:应该每次都将小树并入大树
通过维护一个sz数组来记录每组中树的大小(只需要更新其根节点就行了),sz数组元素初始化为1,即每个节点单独为一个组,当union时将小的并入大的,只需要更改大树的根节点sz的值
package cn.wit;
public class UF
{
private int[] id; // access to component id (site indexed)
private int count; // number of components
private int[]sz;
public UF(int N)
{
// Initialize component id array.
count = N;
id = new int[N];
sz =new int[N];
for (int i = 0; i < N; i++)
id[i] = i;
for (int i = 0; i < N; i++)
sz[i] = 1; // 初始情况下,每个组的大小都是1
}
public int count()
{ return count; }
public boolean connected(int p, int q)
{ return find(p) == find(q); }
private int find(int p)
{
// 寻找p节点所在组的根节点,根节点具有性质id[root] = root
while (p != id[p]) p = id[p];
return p;
}
public void union(int p, int q)
{
int i = find(p);
int j = find(q);
if (i == j) return;
// 将小树作为大树的子树
if (sz[i] < sz[j]) { id[i] = j; sz[j] += sz[i]; }
else { id[j] = i; sz[i] += sz[j]; }
count--;
}
}
Weighted Quick-Union With Path Compression
Path Compression-路径压缩
主要是对find方法的优化,现在树结构find查找次数lgn,find现在是一个一个往上找,一直找到其根节点,Path Compression就是不一个一个的找,跳跃式的找,实现路径的缩短。
仅改进find方法
疑问:那有没有可能最后一次循环爷爷节点跳过了根节点呢?
分析可以知道,即使是跳过了,该节点和根节点的内容是一样的,因为根节点是指向自己的
private int find(int p)
{
// 寻找p节点所在组的根节点,根节点具有性质id[root] = root
while (p != id[p]) {
//该节点的父节点指向该节点的爷爷节点
p = id[id[p]];
}
return p;
}
测试
public static void main(String[] args) {
UF uf = new UF(7);
uf.union(0,1);
uf.union(0,6);
uf.union(0,2);
uf.union(1,6);
uf.union(2,4);
uf.union(4,5);
System.out.println(uf.connected(0,3));//false
}
常用十大算法
非递归二分查找
package suanfa;
public class Search {
public static void main(String[] args) {
int[] arr={1,3,5,5,7,9,11,13,15};
int binarySearch = binarySearch(arr,9);
if(binarySearch == -1){
System.out.println("没有找到该元素");
}else{
System.out.println("该元素在数组中第一次的索引为:"+binarySearch);
}
}
public static int binarySearch(int[] arr, int key) {
int left=0;
int right=arr.length-1;
while(left<=right){
int mid=(left+right)/2;
if(arr[mid]>key){
right=mid-1;
}else if(arr[mid]<key){
left=mid+1;
}else{
return mid;
}
}
return -1;
}
}
分治
分治算法是将一个复杂的问题不断细分成小问题,直到每个问题原子化,在快速排序和归并排序中就使用了分治算法
汉诺塔案例
汉诺塔的步骤是随着盘个数的增加不断变复杂的,但是从最外层来看,将最下面一个盘和上面所有的盘(上面所有盘作为一个整体),完成汉诺塔只有三个步骤:
- 将上面所有的盘移到B
- 将最下面的盘移到C
- 将B所有盘移到C上
然后根据三个步骤将每个问题细分,第一个问题会一直递归,直到问题达到能直接解决的难度,然后merge,回到最外层(在这个过程中实际上是执行了很多个小汉诺塔问题),执行最外层的将最下面的盘移动到C,然后将B所有盘移动到C,也是一直往下递归直到能直接解决的程度,然后merge,回到最外层(在这个过程中实际上是执行了很多个小汉诺塔问题)
package suanfa;
public class HanoiTower {
public static void main(String[] args) {
hanoiTower(5,'A','B','C');
}
public static void hanoiTower(int num, char a,char b,char c) {
if(num == 1){ //一个盘
System.out.println("第一个盘从"+a+"->"+c);
}else{ //最下面一个盘和其他盘
//将A上面所有盘放到B 移动过程会使用到C
hanoiTower(num-1,a,c,b);
//把最下面的盘移动到C
System.out.println("第"+num+"个盘从"+a+"->"+c);
//把B的所有盘放到C上
hanoiTower(num-1,b,a,c);
}
}
}
马踏棋盘算法
马踏棋盘问题是给定一个m *n的棋盘,马走的规则的行走2、列走1,或者行走1、列走2,是否能将棋盘每个格子都走完,如果能走完,给出走棋的顺序
马踏棋盘是一个NP问题,用深度优先搜索+回溯解决,后面进一步给出了贪心优化
回溯
public class HorseChessboard {
private static int X; //表示总列树
private static int Y; //表示总行数
//标记棋盘的各个位置是否被访问过
private static boolean visited[];
//标记前的所有位置都被访问
private static boolean finished; //如果为true,表示成功
public static void main(String[] args) {
System.out.println("骑士周游问题");
X = 8;
Y = 8;
int row = 1;
int col = 1;
int[][] chessboard = new int[X][Y];
visited = new boolean[X * Y];
long start = System.currentTimeMillis();
traversalChessboard(chessboard,row - 1,col - 1,1);
long end = System.currentTimeMillis();
System.out.println("运行时间:" + (end-start) + "ms");
//输出棋盘的最后情况
for(int[]rows : chessboard){
System.out.println(Arrays.toString(rows));
}
}
/**
*
* @param chessboard 棋盘
* @param row 当前行
* @param col 当前列
* @param step 初始位置第一步,步数为X*Y表示成功
*/
public static void traversalChessboard(int[][] chessboard,int row,int col,int step){
chessboard[row][col] = step;
visited[row * X + col] = true;
//获取当前位置可以走的下一个位置的集合(包括已经走过的位置)
ArrayList<Point> ps = next(new Point(col, row));
//遍历
while(!ps.isEmpty()){
Point p = ps.remove(0);//取出一个可以走的位置
//判断该点是否已经访问过
if(!visited[p.y * X + p.x]){ //说明还没有访问过
traversalChessboard(chessboard,p.y,p.x,step + 1);
}
}
//判断马儿是否完成了任务
//在回溯过程中判断该路程是否达到X*Y步,且finished是否为true
//当finished = true后,回溯时step < X * Y 所以需要判断!finished
if(step < X * Y && !finished){
chessboard[row][col] = 0;
visited[row * X + col] = false;
}else{
finished = true;
}
}
/**
* point是java自带对象,表示点,根据当前点,计算马还能走哪些位置,放入集合中
* @param curPoint
* @return
*/
public static ArrayList<Point> next(Point curPoint){
ArrayList<Point> ps = new ArrayList<>();
Point p1 = new Point();
if((p1.x = curPoint.x - 2) >= 0 && (p1.y = curPoint.y - 1) >= 0){
ps.add(new Point(p1));
}
if((p1.x = curPoint.x - 1) >= 0 && (p1.y = curPoint.y - 2) >= 0){
ps.add(new Point(p1));
}
if((p1.x = curPoint.x + 1) < X && (p1.y = curPoint.y - 2) >= 0){
ps.add(new Point(p1));
}
if((p1.x = curPoint.x + 2) < X && (p1.y = curPoint.y - 1) >= 0){
ps.add(new Point(p1));
}
if((p1.x = curPoint.x - 2) >= 0 && (p1.y = curPoint.y + 1) < Y){
ps.add(new Point(p1));
}
if((p1.x = curPoint.x - 1) >= 0 && (p1.y = curPoint.y + 2) < Y){
ps.add(new Point(p1));
}
if((p1.x = curPoint.x + 2) < X && (p1.y = curPoint.y + 1) < Y){
ps.add(new Point(p1));
}
if((p1.x = curPoint.x + 1) < X && (p1.y = curPoint.y + 2) < Y){
ps.add(new Point(p1));
}
return ps;
}
}
使用贪心算法优化解法,我们在选择下一步的时候是直接选择了第一个,如果我们使用贪心策略,选择的节点满足:该节点的下一个节点的选择最少(即next返回的集合最小),那么我们找到符合要求的路径速度就会最快
为什么要这样选取,这是一种局部调整最优的做法,如果优先选择出口多的子结点,那出口少的子结点就会越来越多,很可能出现‘死’结点(顾名思义就是没有出口又没有跳过的结点),这样对下面的搜索纯粹是徒劳,这样会浪费很多无用的时间,反过来如果每次都优先选择出口少的结点跳,那出口少的结点就会越来越少,这样跳成功的机会就更大一些。
public static void sort(ArrayList<Point> ps){
ps.sort(new Comparator<Point>() {
@Override
public int compare(Point o1, Point o2) {
return next(o1).size() > next(o2).size() ? 1:-1;
}
});
}
chessboard[row][col] = step;
visited[row * X + col] = true;
//获取当前位置可以走的下一个位置的集合(包括已经走过的位置)
ArrayList<Point> ps = next(new Point(col, row));
//对ps进行排序,对所有可选点的下一步集合大小进行非递减排序
sort(ps);
动态规划
动态规划(Dynamic Programming)算法的核心思想是:将大问题划分为小问题进行解决,下一个子问题需要重复利用上一次子问题的结果。
0/1背包
背包问题分为0/1背包问题和完全背包问题,区别是0/1背包问题每件物品只有一个,0/1背包问题是通过动态规划解决的,完全背包要简单点,可以用贪心解决。
通过这个图解来理解是怎么填这个表的,动态规划的过程相当于填表的过程,表格的最后一个空,就是背包能放下的最大价值。
public class Pack {
public static void main(String[] args) {
int[] w = {1,4,3}; //重量
int[] val = {1500,3000,2000}; //价值
int m = 4; //背包容量
int n = val.length; //物品个数
int[][] v = new int[n+1][m+1];
//初始化第一行和第一列,这里默认0
for(int i = 1;i < v.length;i++)
for(int j = 1;j < v[0].length;j++){
if(w[i-1] > j){
v[i][j] = v[i-1][j];
}else{
v[i][j] = Math.max(v[i-1][j],val[i-1] + v[i-1][j-w[i-1]]);
}
}
}
}
如果想输出装入背包的是哪些物品,可以用一个path二维数组维护一下,path存储的是物品序号
public class Pack {
public static void main(String[] args) {
int[] w = {1,4,3}; //重量
int[] val = {1500,3000,2000}; //价值
int m = 4; //背包容量
int n = val.length; //物品个数
int[][] v = new int[n+1][m+1];
String[][] path = new String[n+1][m+1];
for(int i = 0;i < path[0].length;i++){
path[0][i] = "";
}
for(int i = 0;i < path.length;i++){
path[i][0] = "";
}
//初始化第一行和第一列,这里默认0
for(int i = 1;i < v.length;i++)
for(int j = 1;j < v[0].length;j++){
if(w[i-1] > j){
v[i][j] = v[i-1][j];
path[i][j] = path[i-1][j] + " ";
}else{
v[i][j] = Math.max(v[i-1][j],val[i-1] + v[i-1][j-w[i-1]]);
if(v[i-1][j] < val[i-1] + v[i-1][j-w[i-1]]){
path[i][j] = path[i-1][j-w[i-1]] + i ;
}else{
path[i][j] = path[i-1][j] + " ";
}
}
}
}
}
字符串匹配
有一个字符串str1,一个字符串str2,求解str1中是否包含str2子串
eg:
String str1 = “BBC ABCDAB ABCDABCDABDE”;
String str2 = “ABCDABD”;
暴力匹配
暴力匹配算法有两个分别指向str1和str2的指针i和j。i指向的位置和j指向的位置匹配时,i++,j++
,即两个指针都向右移动,不匹配时,i = i - (j - 1),j = 0,即i回到本轮匹配中匹配上的最后一个的下一个位置,j重新置为0.当i或者j到达尾部时停止,通过j是否到达尾部判断str2是否为str1的子串。
显然,暴力匹配是低效的,因为如果匹配较长但不是子串的位置,又要重新回溯到匹配开始的地方,最坏时间复杂度是O(m*n)。
package cn.wit;
/**
* 字符串匹配
*/
public class KMP {
public static void main(String[] args) {
System.out.println( violentMatch("1h23hello123","hello"));
}
//暴力匹配算法实现
public static int violentMatch(String str1, String str2){
char[] s1 = str1.toCharArray();
char[] s2 = str2.toCharArray();
int sLen1 = s1.length;
int sLen2 = s2.length;
int i = 0; //指向s1的指针
int j = 0; //指向s2的指针
while(i < sLen1 && j < sLen2){
if(s1[i] == s2[j]){ //匹配成功,两个指针同时后移
i++;
j++;
}else{//没有匹配成功
i = i - (j - 1);
j = 0;
}
}
//根据j的值判断是否匹配成功,如果j走完了,则表示匹配成功
if(j == sLen2){
return i - j;
}
return -1;
}
}
KMP
KMP是一个解决模式串是否在文本串中出现过,如果出现过,得到其最早出现位置的算法。
KMP较之于暴力匹配效率更高的原因在于,当暴力匹配中一轮匹配不成功时,回溯到了之前匹配开始的下一个。而KMP求出部分匹配表,在新一轮匹配决定匹配开始位置的时候,用部分匹配表跳过那些一定匹配不了的值。所以得到部分匹配表并使用部分匹配表决定匹配开始位置是KMP的关键。 部分匹配表的求得属于数学问题,我们这里不深究数学问题。
部分匹配表
package cn.wit;
/**
* 字符串匹配
*/
public class KMP {
public static void main(String[] args) {
String str1 = "BBC ABCDAB ABCDABCDABDE";
String str2 = "ABCDABD";
System.out.println( KMPMatch(str1,str2,kmpNext(str2)));
}
//KMP匹配算法
public static int KMPMatch(String str1, String str2,int[] next) {
for(int i = 0,j = 0;i < str1.length();i++){
//遇到不相等,i不用回溯,i直接往前走,j根据部分匹配表回溯,这使得时间复杂度为O(m+n)
while(j > 0 && str1.charAt(i) != str2.charAt(j)){
j = next[j - 1];
}
if(str1.charAt(i) == str2.charAt(j)){
j++;
}
if(j == str2.length()){
return i - j + 1;
}
}
return -1;
}
//获取目标子串的部分匹配表
//部分匹配值:目标串 第0个元素-第n个元素子串的 前缀 后缀交集长度
public static int[] kmpNext(String dest){
//next数组存储字符串字符对应的部分匹配值
int[] next = new int[dest.length()];
next[0] = 0;
for(int i = 1,j = 0;i < dest.length();i++){
//这句的作用是j前面匹配了一些值,然后遇到不匹配的情况
//使用这个循环能够将其都置为0
while(j > 0 && dest.charAt(i) != dest.charAt(j)){
j = next[j - 1];
}
if(dest.charAt(i) == dest.charAt(j)){
j++;
}
next[i] = j;
}
return next;
}
}
贪心算法
贪心算法是指在对问题进行求解时,在每一步选择中都采取最好或者最优的选择,从而希望能够导致结果是最好或者最优的算法。
集合覆盖
假设存在如下表的需要付费的广播台,以及广播台信号可以覆盖的地区。如何选择最少的广播台,让所有的地区都可以接收到信号
思路分析:
先遍历找出并存储所有的地区allAreas
.
遍历所有的广播电台,找到第一个覆盖了最多未覆盖的地区的电台(用allAreas进行比较)
将这个电台加入到一个集合selects中,把该电台覆盖的地区在所有地区的集合allAreas中去掉
重复直到覆盖所有地区
package cn.wit;
import java.util.*;
public class Greedy {
public static void main(String[] args) {
HashMap<String, HashSet<String>> broadcasts = new HashMap<>();
//将各个电台放入到broadcasts
HashSet<String> hashset1 = new HashSet<>();
hashset1.add("北京");
hashset1.add("上海");
hashset1.add("天津");
HashSet<String> hashset2 = new HashSet<>();
hashset2.add("广州");
hashset2.add("北京");
hashset2.add("深圳");
HashSet<String> hashset3 = new HashSet<>();
hashset3.add("成都");
hashset3.add("上海");
hashset3.add("杭州");
HashSet<String> hashset4 = new HashSet<>();
hashset4.add("上海");
hashset4.add("天津");
HashSet<String> hashset5 = new HashSet<>();
hashset5.add("杭州");
hashset5.add("大连");
broadcasts.put("K1",hashset1);
broadcasts.put("K2",hashset2);
broadcasts.put("K3",hashset3);
broadcasts.put("K4",hashset4);
broadcasts.put("K5",hashset5);
//allAreas存放所有地区
HashSet<String> allAreas = new HashSet<>();
for(HashSet<String> set : broadcasts.values()){
for(String str: set){
allAreas.add(str);
}
}
for(String str: allAreas){
System.out.println(str);
}
//创建ArrayList selects,存放选择的电台集合
ArrayList<String> selects = new ArrayList<>();
//定义一个临时集合,存放遍历过程中的电台覆盖的地区和当前还没有覆盖的地区的交集
HashSet<String> tempSet = new HashSet<>();
//maxKey:保存一次遍历过程中,覆盖最大未覆盖地区电台的key
//如果maxKey不为null,选择该电台,allAreas中remove相应地区,selects中加入该电台
String maxKey = null;
while(allAreas.size() != 0){
maxKey = null;
for(String key : broadcasts.keySet()){
tempSet.clear();
//当前这个key能覆盖的地区
HashSet<String> areas = broadcasts.get(key);
//该电台所有地区放入临时areas
tempSet.addAll(areas);
//该电台覆盖的未覆盖地区
tempSet.retainAll(allAreas);
if(tempSet.size() > 0 && (maxKey == null || tempSet.size() > broadcasts.get(maxKey).size())){
maxKey = key;
}
}
if(maxKey != null){
selects.add(maxKey);
//remove选择电台覆盖的地区
allAreas.removeAll(broadcasts.get(maxKey));
}
}
//输出选择的电台
System.out.print("选择电台:");
for(String key : selects){
System.out.print(key+" ");
}
}
}
DP、贪心、分治分析
动态规划(英语:Dynamic programming,简称 DP)
是一种在数学、管理科学、计算机科学、经济学和生物信息学中使用的,通过把原问题分解为相对简单的子问题的方式求解复杂问题的方法。
动态规划不是某一种具体的算法,而是一种算法思想:若要解一个给定问题,我们需要解其不同部分(即子问题),再根据子问题的解以得出原问题的解。
应用这种算法思想解决问题的可行性,对子问题与原问题的关系,以及子问题之间的关系这两方面有一些要求,它们分别对应了最优子结构和重复子问题。
最优子结构
1.最优子结构规定的是子问题与原问题的关系
动态规划要解决的都是一些问题的最优解,即从很多解决问题的方案中找到最优的一个。当我们在求一个问题最优解的时候,如果可以把这个问题分解成多个子问题,然后递归地找到每个子问题的最优解,最后通过一定的数学方法对各个子问题的最优解进行组合得出最终的结果。总结来说就是一个问题的最优解是由它的各个子问题的最优解决定的。
将子问题的解进行组合可以得到原问题的解是动态规划可行性的关键。在解题中一般用状态转移方程描述这种组合。例如原问题的解为 f(n),其中 f(n) 也叫状态。状态转移方程 f(n) = f(n - 1) + f(n - 2) 描述了一种原问题与子问题的组合关系 。在原问题上有一些选择,不同选择可能对应不同的子问题或者不同的组合方式。例如
f(n) = \left{
f(n−1)+f(n−2)n=2kf(n−1)n=2k+1
f(n−1)+f(n−2)n=2kf(n−1)n=2k+1
\right.
f(n)={ f(n−1)+f(n−2) n=2k
{ f(n−1) n=2k+1
n=2k 和 n = 2k + 1 对应了原问题 n上不同的选择,分别对应了不同的子问题和组合方式。
找到了最优子结构,也就能推导出一个状态转移方程 f(n),通过这个状态转移方程,我们能很快的写出问题的递归实现方法。
2.重复子问题
重复子问题规定的是子问题与子问题的关系。
当我们在递归地寻找每个子问题的最优解的时候,有可能会重复地遇到一些更小的子问题,而且这些子问题会重叠地出现在子问题里,出现这样的情况,会有很多重复的计算,动态规划可以保证每个重叠的子问题只会被求解一次。当重复的问题很多的时候,动态规划可以减少很多重复的计算。
重复子问题不是保证解的正确性必须的,但是如果递归求解子问题时,没有出现重复子问题,则没有必要用动态规划,直接普通的递归就可以了。
例如,斐波那契问题的状态转移方程 f(n) = f(n - 1) + f(n - 2)。在求 f(5)时,需要先求子问题 f(4)f(4) 和 f(3),得到结果后再组合成原问题 f(5) 的解。递归地求 f(4) 时,又要先求子问题 f(3)和 f(2),这里的 f(3)与求 f(5)时的子问题重复了。
分治
解决分治问题的时候,思路就是想办法把问题的规模减小,有时候减小一个,有时候减小一半,然后将每个小问题的解以及当前的情况组合起来得出最终的结果。例如归并排序和快速排序,归并排序将要排序的数组平均地分成两半,快速排序将数组随机地分成两半。然后不断地对它们递归地进行处理。
这里存在有最优的子结构,即原数组的排序结果是在子数组排序的结果上组合出来的,但是不存在重复子问题,因为不断地对待排序的数组进行对半分的时候,两半边的数据并不重叠,分别解决左半边和右半边的两个子问题的时候,没有子问题重复出现,这是动态规划和分治的区别。
贪心
关于最优子结构
贪心:每一步的最优解一定包含上一步的最优解,上一步之前的最优解无需记录
动态规划:全局最优解中一定包含某个局部最优解,但不一定包含上一步的局部最优解,因此需要记录之前的所有的局部最优解
关于子问题最优解组合成原问题最优解的组合方式
贪心:如果把所有的子问题看成一棵树的话,贪心从根出发,每次向下遍历最优子树即可,这里的最优是贪心意义上的最优。此时不需要知道一个节点的所有子树情况,于是构不成一棵完整的树
动态规划:动态规划需要对每一个子树求最优解,直至下面的每一个叶子的值,最后得到一棵完整的树,在所有子树都得到最优解后,将他们组合成答案
结果正确性
贪心不能保证求得的最后解是最佳的,复杂度低
动态规划本质是穷举法,可以保证结果是最佳的,复杂度高