Java算法与数据结构

Java算法与数据结构学习

此文章涉及到的代码及测试类详见我的GitHub:https://github.com/Loserfromlazy/java_data_structure_demo

一.数组

//声明数组
dataType[] arrayRefVar;
//创建数组
arrayRefVar = new dataType[arraySize];
dataType[] arrayRefVar = new dataType[arraySize];
dataType[] arrayRefVar = {value0, value1, ..., valuek};

1.使用自定义类封装数组

public class MyArray{
    private long [] arr;
    //表示数组的有效数据的长度
    private int elements;
    public MyArray(){
        arr =new long [50];
    }
    public MyArray(int maxsize){
        arr =new long[maxsize];
    }
    /**
     * 添加数据
     */
    public void insert(long value){
        arr[elements] =value;
        elemenet++;
    }
    /**
     * 显示数据
     */
    public void display(){
        System.out.println("[");
        for(int i=0;i=elements ||index <0){
            throw new ArrayIndexOutOfBoundsException();
        }else{
            return arr[index];
        }
    }
    /**
     * 删除数据
     */
    public void delete(int index){
        if(index>=elements ||index <0){
            throw new ArrayIndexOutOfBoundsException();
        }else{
            //将之后的数据向前一位进行覆盖
           for(int i=0;i=elements ||index <0){
            throw new ArrayIndexOutOfBoundsException();
        }else{
           for(int i=0;i

2.有序数组

修改上面的添加方法

    /**
     * 添加数据
     */
    public void insert(long value){
        int i;
        for(i=0;ivalue){//找到比新加的值大的那个数的索引号i
                break;
            }
        }
        //将第i位空出,每一位向后面挪一位
        for(int j=elements;j>i;j--){
            arr[j]=arr[j-1];
        }
        //将新插入的值放入该索引的位置
        arr[i]=value;
        elements++;
    }

3.查找算法

线性查找(从头查到尾)

见上面的查找方法

二分法查找(数组必须是有序数组)

	/**
     * 二分法查找数据
     */
	public int binarySearch(long value){
        int middle=0;
        int low=0;//第一位索引
        int pow=elements;//最后一位索引
        while(true){
            middle=(pow+low)/2;
            if(arr[middle]==value){
                return middle;
            }else if(low >pow){//如果第一位索引大于最后一位的索引,代表搜索结束,没有找到该值
                return -1;
            }else{
                if(arr[middle]>value){
		//如果中间的值比查的值大,说明待查值在前面,所以将最后一位的索引改为中间的索引的前一位
                    pow=middle-1;
                }else{
        //如果中间的值比查的值小,说明待查值在后面,所以将第一位的索引改为中间的索引的后一位
                    low=middle+1;
                }
            }
        }
    }

二.简单排序

1.冒泡排序

它重复地走访过要排序的元素列,依次比较两个相邻的元素,如果顺序(如从大到小、首字母从Z到A)错误就把他们交换过来。走访元素的工作是重复地进行直到没有相邻元素需要交换,也就是说该元素列已经排序完成。

    /**
	 * 冒泡排序
	 * @param arr
	 */
	public static void sort(long arr[]) {
		long tmp=0;
		for (int i = 0; i < arr.length-1; i++) {//遍历数组
			//依次比较两个相邻的元素,并交换
            for (int j = arr.length-1; j >i; j--) {
				if(arr[j]

2.选择排序

第一次从待排序的数据元素中选出最小(或最大)的一个元素,存放在序列的起始位置,然后再从剩余的未排序元素中寻找到最小(大)元素,然后放到已排序的序列的末尾。以此类推,直到全部待排序的数据元素的个数为零。选择排序是不稳定的排序方法。

效率比冒泡排序高一些

	/**
	 * 选择排序
	 * @param arr
	 */
	public static void sort(long[] arr) {
		int k=0;
		long tmp=0;//临时存放点
		for (int i = 0; i < arr.length-1; i++) {
			k=i;
            //从剩余的未排序元素中寻找到最小元素存放到临时存放点
			for (int j = i; j < arr.length; j++) {
				if(arr[j]

3.插入排序

每步将一个待排序的记录,按其关键码值的大小插入前面已经排序的文件中适当位置上,直到全部插入完为止。插入排序法,算法适用于少量数据的排序,时间复杂度O(n^2)。是稳定的排序方法。

	/**
	 * 插入排序
	 * @param arr
	 */
	public static void sort(long array[]) {
		int j;
	        //从下标为1的元素开始选择合适的位置插入,因为下标为0的只有一个元素,默认是有序的
	        for(int i = 1 ; i < array.length ; i++){
	            long tmp = array[i];//记录要插入的数据
	            j = i;
	            while(j > 0 && tmp < array[j-1]){//从已经排序的序列最右边的开始比较,找到比其小的数
	                array[j] = array[j-1];//向后挪动
	                j--;
	            }
	            array[j] = tmp;//存在比其小的数,插入
	        }
	}

三. 栈和队列

我们是用数组实现的,在定义数组类型的时候,也就规定了存储在栈或队列中的数据类型,如果想存储不同类型的数据声明为Object(在下一节链表中用Object实现)

3.1栈

栈(stack)又名堆栈,它是一种运算受限的线性表。限定仅在表尾进行插入和删除操作的线性表。这一端被称为栈顶,相对地,把另一端称为栈底。向一个栈插入新元素又称作进栈、入栈或压栈,它是把新元素放到栈顶元素的上面,使之成为新的栈顶元素;从一个栈删除元素又称作出栈或退栈,它是把栈顶元素删除掉,使其相邻的元素成为新的栈顶元素。

栈作为一种数据结构,是一种只能在一端进行插入和删除操作的特殊线性表。它按照先进后出(LIFO, Last In First Out)的原则存储数据,先进入的数据被压入栈底,最后的数据在栈顶,需要读数据的时候从栈顶开始弹出数据(最后一个数据被第一个读出来)。栈具有记忆作用,对栈的插入与删除操作中,不需要改变栈底指针。

java 模拟栈实现

public class MyStack {
	//底层实现是数组
	private long [] arr;
	private int top;
	
	public MyStack() {
		arr=new long[10];
		top=-1;
	}
	
	public MyStack(int maxsize) {
		arr=new long[maxsize];
		top=-1;
	}
	
	/**
	 * 添加数据
	 */
	public void push(int value) {
		arr[++top]= value;
	}
	/**
	 * 移除数据
	 */
	public long pop() {
		return arr[top--];
	}
	
	/**
	 * 查看数据
	 */
	public long peek() {
		return arr[top];
	}
	
	/**
	 * 判断是否为空
	 */
	public boolean isEmpty() {
		return top == -1;
	}
	/**
	 * 判断是否满了
	 */
	public  boolean isFull() {
		return top == arr.length-1;
	}
}

3.2 队列

队列是一种特殊的线性表,特殊之处在于它只允许在表的前端(front)进行删除操作,而在表的后端(rear)进行插入操作,和栈一样,队列是一种操作受限制的线性表。进行插入操作的端称为队尾,进行删除操作的端称为队头。队列中没有元素时,称为空队列。

队列的数据元素又称为队列元素。在队列中插入一个队列元素称为入队,从队列中删除一个队列元素称为出队。因为队列只允许在一端插入,在另一端删除,所以只有最早进入队列的元素才能最先从队列中删除,故队列又称为先进先出(FIFO—first in first out)线性表。

java模拟单向队列

public class MyQueue {
	//底层使用数组
	private long arr[];
	//有效数据
	private int elements;
	//队头
	private int front;
	//队尾
	private int end;
	public MyQueue() {
		arr =new long [10];
		elements=0;
		front=0;
		end=-1;
	}
	public MyQueue(int maxsize) {
		arr =new long [maxsize];
		elements=0;
		front=0;
		end=-1;
	}
	
	/**
	 * 添加数据,队尾插入
	 */
	public void insert(long value) {
		
		arr[++end]=value;
		elements++;
	}
	/**
	 * 删除数据
	 */
	public long remove() {
		elements--;
		return arr[front--];
	}
	/**
	 * 查看数据,从队头查看
	 */
	public long peek() {
		return arr[front];
	}
	/**
	 * 判断是否为空
	 */
	public boolean isEmpty() {
		return elements==0;
	}
	public boolean isFull() {
		return elements ==arr.length;
	}
}

单项队列如果满了,在进行插入会报错,数组溢出。所以改为循环队列。

java模拟循环队列

修改上述添加和删除方法即可

	/**
	 * 添加数据,队尾插入
	 */
	public void insert(long value) {
		if(end==arr.length -1) {//如果数据满了。就从头开始
			end=-1;
		}
		
		arr[++end]=value;
		elements++;
	}
	/**
	 * 删除数据
	 */
	public long remove() {
		long value =arr[front++];
        /*ps:这里是假清除,此处只是将要删除的数据返回将有效数据但未清除
        若想真正清除可以将long数组改为Object然后用如下方式
        Object value=null;
        value =arr[front];
        arr[front] = null;
        front++
        */
		if(front == arr.length) {//如果队头到末尾,从头开始
			front=0;
		}
		elements--;
		return value;
	}

四. 链表

链表是一种物理存储单元上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的。链表由一系列结点(链表中每一个元素称为结点)组成,结点可以在运行时动态生成。每个结点包括两个部分:一个是存储数据元素的数据域,另一个是存储下一个结点地址的指针域。 相比于线性表顺序结构,操作复杂。由于不必须按顺序存储,链表在插入的时候可以达到O(1)的复杂度,比另一种线性表顺序表快得多,但是查找一个节点或者访问特定编号的节点则需要O(n)的时间,而线性表和顺序表相应的时间复杂度分别是O(logn)和O(1)。

4.1单向链表

单链表是链表中结构最简单的。一个单链表的节点(Node)分为两个部分,第一个部分(data)保存或者显示关于节点的信息,另一个部分存储下一个节点的地址。最后一个节点存储地址的部分指向空值。

Java算法与数据结构_第1张图片

java实现单向链表

/**
 * 节点
 * @author Loserfromlazy
 *
 */
public class Node {
	//数据域
	public Object data;
	//节点域
	public Node next;
	
	public Node(Object value) {
		this.data=value;
	}
	
	/**
	 * 显示
	 */
	public void display() {
		System.out.print(data+" ");
	}
}
/**
 * @author Loserfromlazy
 * 链表
 */
public class LinkList {
	//头节点
	private Node first;
	//节点的个数
	private int size;
	public LinkList() {
		first=null;
	}
	
	/**
	 * 插入一个节点,在头节点进行插入
	 */
	public void insertFirst(Object value) {
		Node node = new Node(value);//待插入的节点
		if(size==0) {
			first= node;
		}else {
			node.next=first;//将新插入的节点的下一个节点指向头节点
			first=node;//将新插入的节点变成头节点,这样就可以实现在头节点之前插入节点
		}
		size++;
	}
	
	/**
	 * 删除一个节点,在头节点进行删除
	 */
	public Node deleteFirst() {
		Node node= first;
		first=node.next;//将头节点的下一个节点变成头节点,这样就可以删除当前头节点
		size--;
		return node;
	}
	
	public void display() {
		Node current =first;
		while(current!=null) {
			current.display();
			current=current.next;
		}
		System.out.println();
	}
	
	/**
	 * 查找结点
	 */
	public Node find(Object value) {
		Node current =first;
		int tempSize=size;
		while(tempSize>0) {
			if(value.equals(current.data)){
				return current;
			}else {
				current=current.next;
			}
			tempSize--;
		}
		
		return null;
	}
	
	/**
	 * 删除节点,根据数据域进行删除
	 */
	public boolean delete(Object value) {
		if(size==0) {
			return false;
		}
		Node current =first;
		Node previous =first;
		while(current.data!=value) {//当查到相等的数据current就是要找的节点
			if(current.data==null) {
				return false;
			}else {
                //指向下一个节点继续查
				previous=current;
				current =current.next;
			}
		}
		if(current==first) {
			first=current.next;
			size--;
		}else {
            //将前一个节点直接指向下一个节点,即跳过current节点,就可以完成删除
			previous.next=current.next;
			size--;
		}
		return true;
	}
}

4.2 双端列表

链表中保存着对最后一个链节点的引用。

对于单项链表,如果想在尾部添加一个节点,那么必须从头部一直遍历到尾部,找到尾节点,然后在尾节点后面插入一个节点。如果多个对尾节点的引用,那么会简单很多。

java实现双端列表

主要不同就是新增了一个尾节点,所以修改insertFirst和deleteFirst方法,新增insertLast方法

/**
 * @author Loserfromlazy
 * 双端链表
 */
public class FirstLastLinkList {
	//头节点
	private Node first;
	//尾节点
	private Node last;
	//节点的个数
	private int size;
	public FirstLastLinkList() {
		first=null;
	}
	
	/**
	 * 插入一个节点,在头节点进行插入
	 */
	public void insertFirst(Object value) {
		Node node = new Node(value);
		if(size==0) {
			first=node;
			last= node;//修改的地方
		}else {
			node.next=first;
			first=node;
		}
		size++;
	}
	/**新增的方法
	 * 插入一个节点,从尾节点进行插入
	 */
	public void insertLast(Object value) {
		Node node = new Node(value);
		if(size==0) {
			first=node;
			last=node;
		}else {
            //将待加入的节点插入到尾节点之后,将新插入的节点变成尾节点
			last.next=node;
			last=node;
		}
		size++;
	}
	/**
	 * 删除一个节点,在头节点进行删除
	 */
	public Node deleteFirst() {
		Node node= first;
		if(first.next==null) {//修改的地方,如果头节点没有下一个节点,那么尾节点也为空
			last=null;
		}else {
			first=node.next;
		}
		
		size--;
		return node;
	}
	
	public void display() {
		Node current =first;
		while(current!=null) {
			current.display();
			current=current.next;
		}
		System.out.println();
	}

4.3 双向链表

我们知道单向链表只能从一个方向遍历,那么双向链表它可以从两个方向遍历。

PS:注意双端链表与双向链表的区别:双端链表只是增加了一个指向尾节点的节点,但这个节点没有向回指的指针。索引不能进行双向遍历。而双向链表在每一个节点都增加了向回指的指针所以可以双向遍历。

java双向链表的实现

package ch4;

/**
 * 节点
 * @author Loserfromlazy
 *
 */
public class DoubleNode {
	//数据域
	public Object data;
	//节点域
	public DoubleNode next;
	//向前的指针
	public DoubleNode previous;
	
	public DoubleNode(Object value) {
		this.data=value;
	}
	
	/**
	 * 显示
	 */
	public void display() {
		System.out.print(data+" ");
	}
}
/**
 * @author Loserfromlazy
 * 双端链表
 */
public class DoubleLinkList {
	//头节点
	private DoubleNode first;
	//尾节点
	private DoubleNode last;
	//节点的个数
	private int size;
	public DoubleLinkList() {
		first=null;
		size=0;
		last=null;
	}
	
	/**
	 * 插入一个节点,在头节点进行插入
	 */
	public void insertFirst(Object value) {
		DoubleNode node = new DoubleNode(value);
		if(size==0) {
			first=node;
			last= node;
            size++;
		}else {
            //将头节点的前一个节点变成新插入的节点,然后将新插入的节点的下一个节点指向头节点,这样就可以实现将头节点与新插入的节点相连,然后将新插入的节点变为头节点即可
			first.previous=node;
            node.next=first;
			first=node;
            size++;
		}
		
	
	}
	/**
	 * 插入一个节点,从尾节点进行插入
	 */
	public void insertLast(Object value) {
		DoubleNode node = new DoubleNode(value);
		if(size==0) {
			first=node;
			last=node;
		}else {
			last.next=node;
			node.previous=last;//将尾节点与新插入的节点相连
			last=node;
		}
		size++;
	}
	/**
	 * 删除一个节点,在头节点进行删除
	 */
	public DoubleNode deleteFirst() {
		DoubleNode node= first;
		if(first.next==null) {
			last=null;
		}else {
            //将头节点的下一个节点的前节点变为空这样就切断了两个节点的联系
			first.next.previous=null;
		}
		first=node.next;
		size--;
		return node;
	}
	/**
	 * 删除节点,从尾部进行删除
	 */
	public DoubleNode deleteLast() {
		DoubleNode node= last;
		if(first.next==null) {
			first=null;
		}else {
            //将尾节点的前一个节点的下一个节点变为空这样就切断了两个节点的联系
			last.previous.next=null;
			
		}
		last=last.previous;
		size--;
		return node;
	}
	public void display() {
		DoubleNode current =first;
		while(current!=null) {
			current.display();
			current=current.next;
		}
		System.out.println();
	}
	
	/**
	 * 查找结点
	 */
	public DoubleNode find(Object value) {
		DoubleNode current =first;
		int tempSize=size;
		while(tempSize>0) {
			if(value.equals(current.data)){
				return current;
			}else {
				current=current.next;
			}
			tempSize--;
		}
		
		return null;
	}
	
	/**
	 * 删除节点,根据数据域进行删除
	 */
	public boolean delete(Object value) {
		if(size==0) {
			return false;
		}
		DoubleNode current =first;
		DoubleNode previous =first;
		while(current.data!=value) {
			if(current.data==null) {
				return false;
			}else {
				current =current.next;
			}
		}
		if(current==first) {
			first=current.next;
			size--;
		}else {
			current.previous.next=current.next;//切断联系
			size--;
		}
		return true;
	}
	public boolean isEmpty() {
		if(size==0) {return true;}
		return false;
	}
}

五. 递归 Recursive

5.1 定义

编程语言中,函数Func(Type a,……)直接或间接调用函数本身,则该函数称为递归函数

递归必须满足三个条件:

  1. 在每一次调用自己时,必须是(在某种意义上)更接近于解;
  2. 必须有一个终止处理或计算的准则。

当边界条件不满足,递归前进,不满足递归返回;

5.2 三角数字

在该数列中第n项由第n-1项加第n项得到:

三角数即正整数前n项和: 1, 3, 6, 10, 15, 21, 28, 36, 45, 55, 66, 78,..n(n+1)/2

比如:1,1+2=3,1+2+3=6........

java实现三角数字

public static int getNumber(int n){//n为第几项
	int total =0;
    while(n>0){
        total=total+n;
        n--;
    }
    return total;
}

java递归实现三角数字

public static int getNumberByRecursive(int n){//n为第几项
    if(n==1){
        return 1;
    }else{
        return n+getNumberByRecursive(n-1);
    }
}

5.3 Fibonacci数列

斐波那契数列(Fibonacci sequence),又称黄金分割数列、因列昂纳多·斐波那契(Leonardoda Fibonacci)以兔子繁殖为例子而引入,故又称为“兔子数列”,指的是这样一个数列:1、1、2、3、5、8、13、21、34、……

java递归方法实现

此方法的空间复杂度O(2^n)越来越高,最终达到最大深度导致栈溢出。一般不用此方法实现斐波那契数列。

/**
* 递归方法实现
* f(n) = f(n - 1) + f(n - 2)
* 最高支持 n = 92 ,否则超出 Long.MAX_VALUE
* @param num n 
* @return f(n) 
*/
public static long Fibonacci(int n) {
    if(n < 1)
        return 0;
    if(n < 3)
        return 1;
    return fibRec(n - 1) + fibRec(n - 2);
}

对上述方法改良:用平推方法实现(此方法不做深究,我这里主要是举递归的例子)

public static long fibLoop(int num) {
    if(num < 1 || num > 92)
        return 0;
    long a = 1;
    long b = 1;
    long temp;
    for(int i = 3; i <= num; i++) {
        temp = a;
        a = b;
        b += temp;
    }
    return b;
}

5.4 汉诺塔

汉诺塔:汉诺塔(又称河内塔)问题是源于印度一个古老传说的益智玩具。大梵天创造世界的时候做了三根金刚石柱子,在一根柱子上从下往上按照大小顺序摞着64片黄金圆盘。大梵天命令婆罗门把圆盘从下面开始按大小顺序重新摆放在另一根柱子上。并且规定,在小圆盘上不能放大圆盘,在三根柱子之间一次只能移动一个圆盘。

Java算法与数据结构_第2张图片

使用递归思想解决汉诺塔

如果有三个盘子,只要将最上面的两个盘子解决。如果有四个盘子,只要将最上面的三个盘子解决。依次类推无论有多少个盘子,我们都将其看做只有两个盘子。简单来说,递归算法:

  1. 从初始塔座A上移动包含n-1个盘子到中介塔座B上。
  2. 将初始塔座A上剩余的一个盘子(最大的一个盘子)放到目标塔座C上。
  3. 将中介塔座B上n-1个盘子移动到目标塔座C上。

java实现汉诺塔

	/**
	 * 移动盘子
	 * @param topN 移动的盘子数
	 * @param from 起始塔座
	 * @param inter 辅助塔座
	 * @param to	目标塔座
	 */
	public static void doTower(int topN,String from,String inter,String to) {
		if(topN==1) {
			System.out.println("盘子"+topN+"从"+from+"塔座到"+to+"塔座");
		}else {
			doTower(topN-1,from,to,inter);
			System.out.println("盘子"+topN+"从"+from+"塔座到"+to+"塔座");
			doTower(topN-1, inter, from, to);
		}
	}

5.5 整数划分

此问题是求正整数n表示成一系列正整数的和

将最大加数n不大于m的的划分记作q(n,m),比如q(6,4)就是把6用小于等于4的数进行划分

6

5+1

4+2,4+1+1

3+3,3+2+1,3+1+1

2+2+2,2+2+1+1,2+1+1+1+1

1+1+1+1+1+1

具体递归如下图

当m=1,n=1明显就是1;

当n

当n=m时就是q(n,n),这需要继续递归所以q(n,m)=q(n,m-1),如果不理解看上面q(6,6)=q(6,5)+1

当n>m时,q(n,m)=q(n,m-1)+q(n-m,n-m),如果不理解看上面q(6,4)有四行,可以将其分为两部分q(6,3)和q(2,4)包括(4+2,4+1+1;)q(2,4)可以写成q(2,2)

Java算法与数据结构_第3张图片

java代码

public class Huafen {
    public static void main(String[] args) {
        System.out.println(split(6));
    }
    public static int split(int n) {
        return p(n,n);
    }
    public static int p(int n,int m) {
        if(n==1 || m==1) {
            return 1;
        } else if(nm) {
            return  p(n,m-1) + p(n-m,m);
        }
        return -1;
    }
}
结果为11

5.6 棋盘覆盖

解决思路:应用分治法

分治的技巧在于如何划分棋盘,使划分后的子棋盘的大小相同,并且每个子棋盘均包含一个特殊方格,从而将原问题分解为规模较小的棋盘覆盖问题。k>0 时,可将2k×2k的棋盘划分为4个2(k-1)×2(k-1)的子棋盘。这样划分后,由于原棋盘只有一个特殊方格,所 以,这4个子棋盘中只有一个子棋盘包含该特殊方格,其余3个子棋盘中没有特殊方格。为了将这3个没有特殊方格的子棋盘转化为特殊棋盘,以便采用递归方法求 解,可以用一个L型骨牌覆盖这3个较小棋盘的会合处,从而将原问题转化为4个较小规模的棋盘覆盖问题。递归地使用这种划分策略, 直至将棋盘分割为1×1的子棋盘。

java核心代码:

/**
 * 棋盘覆盖问题
 * @author  Loserfromlazy
 */
public class ChessBoard {
    private static int[][] board ;
    int tile=0;//表示L型骨牌的编号

    /**
     * 棋盘覆盖函数
     * @param tr 棋盘左上角行号
     * @param tc 棋盘左上角列号
     * @param dr 特殊棋子的行
     * @param dc 特殊棋子列
     * @param size 棋盘大小
     */
    public void chessBoard(int tr,int tc,int dr,int dc,int size){
        if (size==1) return;
        int t=++tile;
        int s = size/2;//每一次将大棋盘化为一半的子棋盘
        //处理左上角棋盘
        if(dr < tr + s && dc< tc + s)//左上角子棋盘有特殊棋子
            chessBoard(tr,tc,dr,dc,s);//递归处理
        else//处理无特殊棋子的左上角子棋盘
        {
            board[tr+s-1][tc+s-1] = t;//设左上角子棋盘的右下角为特殊棋子,用t型的骨牌覆盖。
            chessBoard(tr,tc,tr+s-1,tc+s-1, s);//递归处理
        }
        //处理右上角棋盘
        if(dr < tr+s && dc >=tc+s)//右上角棋盘有特殊棋子
        {
            chessBoard(tr,tc+s,dr,dc,s);//递归处理
        }
        else
        {
            board[tr+s-1][tc+s] =t;//设右上角子棋盘的左下角为特殊棋子,用t型的骨牌覆盖。
            chessBoard(tr,tc+s,tr+s-1,tc+s,s);//递归处理
        }
        //处理左下角棋盘
        if(dr >=tr+s && dc=tr+s&& dc>= tc+s)//右下角棋盘有特殊棋子
        {
            chessBoard(tr+s,tc+s,dr,dc,s);//递归处理
        }
        else
        {
            board[tr+s][tc+s] = t;//设子棋盘右下角的左上角为特殊棋子,用t型的骨牌覆盖。
            chessBoard(tr+s,tc+s,tr+s,tc+s,s);//递归处理
        }
    }

    public static void main(String[] args) {
        详见我的github
    }
}

六. 希尔排序和快速排序

希尔排序

希尔排序(Shell's Sort)是插入排序的一种又称“缩小增量排序”(Diminishing Increment Sort),是直接插入排序算法的一种更高效的改进版本。希尔排序是非稳定排序算法。该方法因D.L.Shell于1959年提出而得名。(直接插入排序见上面)

希尔排序是把记录按下标的一定增量分组,对每组使用直接插入排序算法排序;随着增量逐渐减少,每组包含的关键词越来越多,当增量减至1时,整个文件恰被分成一组,算法便终止。

缺陷:假如一个很小的的数据在靠有的排序上,那么要将该数据排序到正确的位置上,则,所有的中间数据都需要向右移动一位。

优点:希尔排序通过加大插入排序中元素之间的间隔,并对这些间隔的元素进行插入排序,从而使得数据可以大幅度的移动。当完成该间隔的排序后,希尔排序会减少数据的间隔在进行排序。依次进行下去。

希尔排序间隔的计算

希尔的原稿中,他建议间隔选为N/2。但是间隔的初始值为1,通过计算最大间隔,直到该间隔大于数组的大小时停止,最大间隔为不大于数组大小的最大值,这是最好的间隔序列。

最好的间隔序列h=h*3+1。即1,4,13,40...序列。间隔的减少:h=(h-1)/3来计算

java实现希尔排序

public class ShellSort {
	public static void sort(long[] arr) {
		// 初始化间隔
		int h = 1;
		// 计算最大间隔
		while (h < arr.length / 3) {
			h = h * 3 + 1;
		}
		while (h > 0) {
			// 进行插入排序
			for (int i = h; i < arr.length; i++) {// 从最大间隔开始进行排序
				long temp = arr[i];
				int j = i;
				while (j > h - 1 && temp <= arr[j - h]) {
                    // 如果比同间隔小则交换两个数,比如a[4]

快速排序

基于分治的思想,是冒泡排序的改进型。快速排序通过将一个数组划分为两个子数组,然后通过调用自身为每一个子数组进行快速排序。

如何进行划分:主要是设定关键字,将比关键字小的数据放在一组,比关键字大的数据放在一组。

/**
 * 快速排序
 */
public class QuickSort {
    /**
     * 划分数组
     * @param arr
     * @param left  起点位置
     * @param right 终点位置
     *
     */
    public static int partition(long arr[],int left,int right){
        int leftPtr = left;//关键字为arr[left],所以下面++leftPtr,及跳过关键字
        int rightPtr=right+1;//下面为--rightPtr,所以此处加1
        long point =arr[left];
        while(true){
            //循环,将比关键字小的留在左边
            while (leftPtr < right && arr[++leftPtr] < point);
            //循环,将比关键字大的留在右边
            while (rightPtr > 0 && arr[--rightPtr] > point);
            if (leftPtr>=rightPtr){//左右游标相遇时停止
                break;
            }else{//为相遇时交换元素
                swap(arr,leftPtr,rightPtr);
            }
        }
        //最后交换左右游标相遇时的所指的元素与关键字交换
        swap(arr,left,rightPtr);
        return rightPtr;//返回关键字位置
    }
    public static void sort(long arr[],int left,int right){
        //设置跳出条件
        if(left>=right){
            return;
        }else {

            int part=partition(arr,left,right);
            sort(arr,left,part-1);//对上一轮排序,关键字左边的子数组递归
            sort(arr,part+1,right);//对上一轮排序,关键字右边的子数组递归 ;
        }
    }
    /**
     * 显示数组
     * @param arr
     */
    public static void display(long [] arr){
        for (long x : arr) {
            System.out.print(x+"  ");
        }
        System.out.println();
    }
    /**
     * 交换
     * @param arr
     * @param i
     * @param j
     */
    public static void swap(long [] arr,int i,int j){
        long tmp=arr[i];
        arr[i]=arr[j];
        arr[j]=tmp;
    }
}

七. 二叉树

7.1树

树是由根结点和若干颗子树构成的。树是由一个集合以及在该集合上定义的一种关系构成的。集合中的元素称为树的结点,所定义的关系称为父子关系。父子关系在树的结点之间建立了一个层次结构。在这种层次结构中有一个结点具有特殊的地位,这个结点称为该树的根结点,或称为树根。(图片来源于网络)

为什么需要树?

有序数组插入或删除数据慢;链表查找数据太慢

树中能快速插入删除及查找数据

Java算法与数据结构_第4张图片

空集合也是树,称为空树。空树中没有结点。

结点的度:一个结点含有的子结点的个数称为该结点的度;

叶结点或终端结点:度为0的结点称为叶结点;

非终端结点或分支结点:度不为0的结点;

双亲结点或父结点:若一个结点含有子结点,则这个结点称为其子结点的父结点;

孩子结点或子结点:一个结点含有的子树的根结点称为该结点的子结点;

兄弟结点:具有相同父结点的结点互称为兄弟结点;

树的度:一棵树中,最大的结点的度称为树的度;

结点的层次:从根开始定义起,根为第1层,根的子结点为第2层,以此类推;

树的高度或深度:树中结点的最大层次;

堂兄弟结点:双亲在同一层的结点互为堂兄弟;

结点的祖先:从根到该结点所经分支上的所有结点;

子孙:以某结点为根的子树中任一结点都称为该结点的子孙。

森林:由m(m>=0)棵互不相交的树的集合称为森林;

7.2 二叉树

树的每个节点最多只能有两个子节点

二叉树是递归定义的,其结点有左右子树之分,逻辑上二叉树有五种基本形态:

Java算法与数据结构_第5张图片

(1)空二叉树——如图(a);

(2)只有一个根结点的二叉树——如图(b);

(3)只有左子树——如图(c);

(4)只有右子树——如图(d);

(5)完全二叉树——如图(e)。

注意:尽管二叉树与树有许多相似之处,但二叉树不是树的特殊情形。

7.3 二叉排序树

二叉排序树(Binary Sort Tree),又称二叉查找树Binary Search Tree),亦称二叉搜索树。是数据结构中的一类。在一般情况下,查询效率比链表结构要高。

二叉排序树是一棵空树,或者是具有下列性质的二叉树

(1)若左子树不空,则左子树上所有结点的值均小于它的根结点的值;

(2)若右子树不空,则右子树上所有结点的值均大于它的根结点的值;

(3)左、右子树也分别为二叉排序树;

(4)没有键值相等的结点。

/**
 * 二叉树节点
 */
public class Node {
    //数据项,设置为default可以同一个包访问,方便tree访问
    long data;
    //左子节点
    Node leftChild;
    //右子节点
    Node rightChild;

    public Node(long data){
        this.data=data;
    }
    //输出数据项
    public void display(){
        System.out.println(data);
    }
}
这里是两个文件
/**
 * 二叉树类
 */
public class Tree {
    //根节点,测试Tree类时可以改为public,查看数据
    private Node root;
    /**
     * 插入节点
     * @param value
     */
    public void insert(long value){
    }
    /**
     * 查找节点
     * @param value
     */
    public void find(long value){
    }
    /**
     * 删除节点
     * @param value
     */
    public void delete(long value){
    }
    //。。。。
}

7.4 二叉树的插入和查找

插入节点

从根节点开始查找一个相应的节点,这个节点将成为新插入既然点的父节点,当父节点找到后,通过判断新节点的值比父节点的值的大小来决定链接到左子节点还是右子节点。

java代码如下,可直接放到上面插入节点代码处

/**
 * 插入节点
 * @param value
 */
public void insert(long value){
    //封装节点
    Node newNode = new Node(value);
    //引用当前节点
    Node current =root;
    //引用父节点
    Node parent;
    //如果root为null,第一次插入时
    if (root == null){
        root = newNode;
    }else {
        while (true) {
            //父节点指向当前节点
            parent = current;
            //如果当前节点数据比插入的大,则向左走
            if (current.data > value) {
                current = current.leftChild;
                if (current == null){//如果左子节点为空
                    parent.leftChild = newNode;//插入新值,返回
                    return;
                }
            } else {
                current = current.rightChild;
                if (current == null){//如果右子节点为空
                    parent.rightChild = newNode;//插入新值,返回
                    return;
                }
            }
        }
    }
}

查找结点

从根节点开始查找,如果查找的节点值比当前节点小,则继续查左子树,否则查找右子树。

Java代码如下:

/**
 * 查找节点
 * @param value
 */
public Node find(long value){
    //引用当前节点,从根节点开始
    Node current = root;
    //循环,查找值不等于当前节点的数据项
    while (current.data != value){
        //比较当前节点大小与查找值
        if (current.data > value){
            current = current.leftChild;
        }else if(current.data < value) {
            current =current.rightChild;
        }else {
            return null;
        }
    }
    return current;
}

7.5 遍历树

遍历树是根据一种特定的顺序访问树的每一个节点。比较常用的有前序遍历,中序遍历和后序遍历。而二叉搜索树最常用的是中序遍历。

前序遍历:(根左右)访问根节点——>前序遍历左子树——>前序遍历右子树

中序遍历(左根右):中序遍历左子树——>访问根节点——>中序遍历右子树

后序遍历(左右根):后序遍历左子树——>后序遍历右子树——>访问根节点

Java算法与数据结构_第6张图片

前序遍历:30-20-15-20-40-35-50

中序遍历:15-20-25-30-35-40-50

后序遍历:15-25-20-35-50-40-30

java代码如下:

package ch7;

/**
 * 二叉树类
 */
public class Tree {
    //根节点
    public Node root;

    /**
     * 插入节点
     * @param value
     */
    public void insert(long value){
        //封装节点
        Node newNode = new Node(value);
        //引用当前节点
        Node current =root;
        //引用父节点
        Node parent;
        //如果root为null,第一次插入时
        if (root == null){
            root = newNode;
        }else {
            while (true) {
                //父节点指向当前节点
                parent = current;
                //如果当前节点数据比插入的大,则向左走
                if (current.data > value) {
                    current = current.leftChild;
                    if (current == null){//如果左子节点为空
                        parent.leftChild = newNode;//插入新值,返回
                        return;
                    }
                } else {
                    current = current.rightChild;
                    if (current == null){//如果右子节点为空
                        parent.rightChild = newNode;//插入新值,返回
                        return;
                    }
                }
            }
        }
    }

    /**
     * 查找节点
     * @param value
     */
    public Node find(long value){
        //引用当前节点,从根节点开始
        Node current = root;
        //循环,查找值不等于当前节点的数据项
        while (current.data != value){
            //比较当前节点大小与查找值
            if (current.data > value){
                current = current.leftChild;
            }else if(current.data < value) {
                current =current.rightChild;
            }else {
                return null;
            }
        }
        return current;
    }

    /**
     * 前序遍历
     * @param localNode
     */
    public void frontOrder(Node localNode){
        if (localNode != null){
            //访问根节点
            System.out.println(localNode.data+" ");
            //前序遍历左子树
            frontOrder(localNode.leftChild);
            //前序遍历右子树
            frontOrder(localNode.rightChild);
        }
    }

    /**
     * 中序遍历
     * @param localNode
     */
    public void inOrder(Node localNode){
        if (localNode != null){
            //中序遍历左子树
            inOrder(localNode.leftChild);
            //访问根节点
            System.out.println(localNode.data+" ");
            //中序遍历右子树
            inOrder(localNode.rightChild);
        }
    }
    /**
     * 后序遍历
     * @param localNode
     */
    public void afterOrder(Node localNode){
        if (localNode != null){
            //后序遍历左子树
            afterOrder(localNode.leftChild);
            //后序遍历右子树
            afterOrder(localNode.rightChild);
            //访问根节点
            System.out.println(localNode.data+" ");
        }
    }

    /**
     * 查找中序后继节点
     * @param delNode
     * @return
     */
    public Node getSuccessor(Node delNode){
        Node successor =delNode;
        Node successorParent = delNode;
        Node current =delNode.rightChild;

        while (current != null){
            successorParent = successor;//保存父节点的引用
            successor =current;//保存当前节点的引用
            current = current.leftChild;
        }
        //将中序后继节点与删除节点替换
        if (successor != delNode.rightChild){
            successorParent.leftChild = successor.rightChild;//如果这个中序后继节点还有右子节点,将他变成这个中序后继节点位置
            successor.rightChild = delNode.rightChild;
        }
        return successor;
    }
    /**
     * 删除节点
     * @param value
     */
    public boolean delete(long value){
        //引用当前节点
        Node current =root;
        //引用当前节点父节点
        Node parent = root;
        //判断是否是左子节点
        boolean isLeftChild = false;
        //查找节点
        while (current.data!= value){
            parent=current;
            if (current.data > value){
                current = current.leftChild;
                isLeftChild = true;
            }else if(current.data < value) {
                current =current.rightChild;
                isLeftChild = false;
            }else {
                return false;
            }
        }

        //删除节点
        if (current.leftChild == null && current.rightChild ==null){//删除叶子节点,该节点没有子节点
            if (current == root){
                root = null;
            }
            //如果是父节点的左子节点
            else if (isLeftChild){
                parent.leftChild=null;
            }else {
                parent.rightChild=null;
            }
            return true;
        }else if (current.rightChild == null){//如果节点只有一个左节点
            if (current == root){//跳过待删除节点,让待删除节点的左子节点变成根节点
                root=current.leftChild;
            }
            else if (isLeftChild){//如果待删除节点是左子节点,让父节点的左子节点等于待删除节点的左子节点
                parent.leftChild=current.leftChild;
            }else {//如果待删除节点是右子节点,让父节点的右子节点等于待删除节点的左子节点
                parent.rightChild=current.leftChild;
            }
            return true;
        }else if (current.leftChild == null){//如果节点只有一个右节点
            if (current == root){//跳过待删除节点,让待删除节点的右子节点变成根节点
                root=current.rightChild;
            }else if (isLeftChild){//如果待删除节点是左子节点,让父节点的左子节点等于待删除节点的右子节点
                parent.leftChild =current.rightChild;
            }else {
                parent.rightChild=current.rightChild;
            }
            return true;
        }else {//如果有两个子节点
            Node successor =getSuccessor(current);
            if (current == root){
                root =successor;
            }else if (isLeftChild){
                parent.leftChild=successor;
            }else {
                parent.rightChild=successor;
            }
            successor.leftChild=current.leftChild;

        }
        return false;
    }
}

7.6 删除节点

删除节点非常复杂,在删除钱首先要查找要删除的节点,找到节点后还有三种情况:

  1. 该节点是叶子节点,没有子节点

    要删除叶子节点,只需要改变该节点的父节点的引用值为null即可。

    Java算法与数据结构_第7张图片

  2. 该节点有一个子节点

    改变父节点的引用,将其直接指向要删除节点的子节点

    Java算法与数据结构_第8张图片

  3. 该节点有两个字节点

    需要使用它的中序后继来代替该节点。实际上就是要找比删除节点关键值大的节点集合中最小的一个节点,只有这样代替删除节点后才能满足二叉搜索树的特性。

    程序找到删除节点的右节点,然后转到该右节点的左子节点,依次顺着左子节点找下去,最后一个左子节点即是后继节点;如果该右节点没有左子节点,那么该右节点便是后继节点。

    后继节点也就是:比删除节点大的最小节点。

    Java算法与数据结构_第9张图片

java代码:

package ch7;
/**
 * 二叉树类
 */
public class Tree {
    //根节点
    public Node root;

    /**
     * 插入节点
     * @param value
     */
    public void insert(long value){
        //封装节点
        Node newNode = new Node(value);
        //引用当前节点
        Node current =root;
        //引用父节点
        Node parent;
        //如果root为null,第一次插入时
        if (root == null){
            root = newNode;
        }else {
            while (true) {
                //父节点指向当前节点
                parent = current;
                //如果当前节点数据比插入的大,则向左走
                if (current.data > value) {
                    current = current.leftChild;
                    if (current == null){//如果左子节点为空
                        parent.leftChild = newNode;//插入新值,返回
                        return;
                    }
                } else {
                    current = current.rightChild;
                    if (current == null){//如果右子节点为空
                        parent.rightChild = newNode;//插入新值,返回
                        return;
                    }
                }
            }
        }
    }

    /**
     * 查找节点
     * @param value
     */
    public Node find(long value){
        //引用当前节点,从根节点开始
        Node current = root;
        //循环,查找值不等于当前节点的数据项
        while (current.data != value){
            //比较当前节点大小与查找值
            if (current.data > value){
                current = current.leftChild;
            }else if(current.data < value) {
                current =current.rightChild;
            }else {
                return null;
            }
        }
        return current;
    }

    /**
     * 前序遍历
     * @param localNode
     */
    public void frontOrder(Node localNode){
        if (localNode != null){
            //访问根节点
            System.out.println(localNode.data+" ");
            //前序遍历左子树
            frontOrder(localNode.leftChild);
            //前序遍历右子树
            frontOrder(localNode.rightChild);
        }
    }

    /**
     * 中序遍历
     * @param localNode
     */
    public void inOrder(Node localNode){
        if (localNode != null){
            //中序遍历左子树
            frontOrder(localNode.leftChild);
            //访问根节点
            System.out.println(localNode.data+" ");
            //中序遍历右子树
            frontOrder(localNode.rightChild);
        }
    }
    /**
     * 后序遍历
     * @param localNode
     */
    public void afterOrder(Node localNode){
        if (localNode != null){
            //后序遍历左子树
            frontOrder(localNode.leftChild);
            //后序遍历右子树
            frontOrder(localNode.rightChild);
            //访问根节点
            System.out.println(localNode.data+" ");
        }
    }

    /**
     * 查找中序后继节点
     * @param delNode
     * @return
     */
    public Node getSuccessor(Node delNode){
        Node successor =delNode;
        Node successorParent = delNode;
        Node current =delNode.rightChild;

        while (current != null){
            successorParent = successor;//保存父节点的引用
            successor =current;//保存当前节点的引用
            current = current.leftChild;
        }
        //将中序后继节点与删除节点替换
        if (successor != delNode.rightChild){
            successorParent.leftChild = successor.rightChild;//如果这个中序后继节点还有右子节点,将他变成这个中序后继节点位置
            successor.rightChild = delNode.rightChild;
        }
        return successor;
    }
    /**
     * 删除节点
     * @param value
     */
    public boolean delete(long value){
        //引用当前节点
        Node current =root;
        //引用当前节点父节点
        Node parent = root;
        //判断是否是左子节点
        boolean isLeftChild = false;
        //查找节点
        while (current.data!= value){
            parent=current;
            if (current.data > value){
                current = current.leftChild;
                isLeftChild = true;
            }else if(current.data < value) {
                current =current.rightChild;
                isLeftChild = false;
            }else {
                return false;
            }
        }

        //删除节点
        if (current.leftChild == null && current.rightChild ==null){//删除叶子节点,该节点没有子节点
            if (current == root){
                root = null;
            }
            //如果是父节点的左子节点
            else if (isLeftChild){
                parent.leftChild=null;
            }else {
                parent.rightChild=null;
            }
            return true;
        }else if (current.rightChild == null){//如果节点只有一个左节点
            if (current == root){//跳过待删除节点,让待删除节点的左子节点变成根节点
                root=current.leftChild;
            }
            else if (isLeftChild){//如果待删除节点是左子节点,让父节点的左子节点等于待删除节点的左子节点
                parent.leftChild=current.leftChild;
            }else {//如果待删除节点是右子节点,让父节点的右子节点等于待删除节点的左子节点
                parent.rightChild=current.leftChild;
            }
            return true;
        }else if (current.leftChild == null){//如果节点只有一个右节点
            if (current == root){//跳过待删除节点,让待删除节点的右子节点变成根节点
                root=current.rightChild;
            }else if (isLeftChild){//如果待删除节点是左子节点,让父节点的左子节点等于待删除节点的右子节点
                parent.leftChild =current.rightChild;
            }else {
                parent.rightChild=current.rightChild;
            }
            return true;
        }else {//如果有两个子节点
            Node successor =getSuccessor(current);
            if (current == root){
                root =successor;
            }else if (isLeftChild){
                parent.leftChild=successor;
            }else {
                parent.rightChild=successor;
            }
            successor.leftChild=current.leftChild;

        }
        return false;
    }
}

7.7 哈夫曼树和哈夫曼编码

哈夫曼编码(Huffman Coding)是一种编码方法,哈夫曼编码是可变字长编码(VLC)的一种。

哈夫曼编码使用变长编码表对源符号(如文件中的一个字母)进行编码,其中变长编码表是通过一种评估来源符号出现机率的方法得到的,出现机率高的字母使用较短的编码,反之出现机率低的则使用较长的编码,这便使编码之后的字符串的平均长度、期望值降低,从而达到无损压缩数据的目的。

哈夫曼编码需要依赖哈夫曼树来实现,哈夫曼树又称为最优二叉树,哈夫曼树是带权路径长度最小的树。

下面演示创建哈夫曼树

假设有a b c d e五个字符,权重分别为50, 10, 16, 8, 12,现在将它们生成一颗哈夫曼树。

先找出权重最小的两个字符构成一棵二叉树,这里最小的为d 和 b

Java算法与数据结构_第10张图片

它们父节点的权重是两者相加的结果,现在将b和d的权重从最序列( 50, 10, 16, 8, 12)中删除,再将它们的父节点的权重加入为50,16,12,18重复直到序列中只有一个元素为止

最后生成的哈夫曼树为

Java算法与数据结构_第11张图片

可见,根节点到权重最高的字符所需的路径最短。

八. 红黑树

红黑树(Red Black Tree) 是一种自平衡二叉查找树,是在计算机科学中用到的一种数据结构,典型的用途是实现关联数组。

8.1 简介

二叉树的问题

二叉树作为数据存储工具有很大的优势,可以快速插入删除和查找数据项,但这仅仅针对于插入随机数据,如果插入的数据是有序的,那么速度将变得特别慢。

如下图,如果是顺序数组,那么会造成二叉树的右节点为空造成浪费。

Java算法与数据结构_第12张图片

九.哈希表

十. 图

你可能感兴趣的:(Java算法与数据结构)