Java算法笔记

基础数据结构

数组

基本概念

public class TestOpArray {
	
	public static void main(String[] args) {
		//解决数组的长度不可变的问题
		int[] arr = new int[] {9,8,7};
		//快速查看数组中的元素
		System.out.println(Arrays.toString(arr));
		//要加入数组的目标元素
		int dst=6;
		
		//创建一个新的数组,长度是原数组长度+1
		int[] newArr = new int[arr.length+1];
		//把原数组中的数据全部复制到新数组中
		for(int i=0;i<arr.length;i++) {
			newArr[i]=arr[i];
		}
		//把目标元素放入新数组的最后
		newArr[arr.length]=dst;
		//新数组替换原数组
		arr=newArr;
		System.out.println(Arrays.toString(arr));
	}
	
}


public class TestOpArray2 {
	//如何删除数组中的元素
	public static void main(String[] args) {
		//目标数组
		int[] arr = new int[] {9,8,7,6,5,4};
		//要删除的元素的下标
		int dst = 3;
		System.out.println(Arrays.toString(arr));
		
		//创建一个新的数组,长度是原数组的长度-1
		int[] newArr = new int[arr.length-1];
		//复制原数组中除了要删除的那个元素以外其它的元素
		for(int i=0;i<newArr.length;i++) {
			//要删除的元素之前的元素
			if(i<dst) {
				newArr[i]=arr[i];
			//要删除的元素之后的元素
			}else {
				newArr[i]=arr[i+1];
			}
		}
		//新数组替换旧数组
		arr=newArr;
		System.out.println(Arrays.toString(arr));
	}
	
}


public class MyArray {

	// 用于存储数据的数组
	private int[] elements;

	public MyArray() {
		elements = new int[0];
	}

	// 获取数组长度的方法
	public int size() {
		return elements.length;
	}

	// 往数组的末尾添加一个元素
	public void add(int element) {
		// 创建一个新的数组
		int[] newArr = new int[elements.length + 1];
		// 把原数组中的元素复制到新数组中
		for (int i = 0; i < elements.length; i++) {
			newArr[i] = elements[i];
		}
		// 把添加的元素放入新数组中
		newArr[elements.length] = element;
		// 使用新数组替换旧数组
		elements = newArr;
	}

	// 打印所有元素到控制台
	public void show() {
		System.out.println(Arrays.toString(elements));
	}

	// 删除数组中的元素
	public void delete(int index) {
		// 判断下标是否越界
		if (index < 0 || index > elements.length - 1) {
			throw new RuntimeException("下标越界");
		}
		// 创建一个新的数组,长度为原数组的长度-1
		int[] newArr = new int[elements.length - 1];
		// 复制原有数据到新数组
		for (int i = 0; i < newArr.length; i++) {
			// 想要删除的元素前面的元素
			if (i < index) {
				newArr[i] = elements[i];
				// 想要删除的元素后面的元素
			} else {
				newArr[i] = elements[i + 1];
			}
		}
		// 新数组替换旧数组
		elements = newArr;
	}

	// 取出指定位置的元素
	public int get(int index) {
		// 判断下标是否越界
		if (index < 0 || index > elements.length - 1) {
			throw new RuntimeException("下标越界");
		}
		return elements[index];
	}

	// 插入一个元素到指定位置
	public void insert(int index, int element) {
		// 创建一个新的数组
		int[] newArr = new int[elements.length + 1];
		// 将原数组中的元素放入新数组中。
		for (int i = 0; i < elements.length; i++) {
			// 目标位置之前的元素
			if (i < index) {
				newArr[i] = elements[i];
				// 目标位置之后的元素
			} else {
				newArr[i + 1] = elements[i];
			}
		}
		// 插入新的元素
		newArr[index] = element;
		// 新数组替换旧数组
		elements = newArr;
	}

	// 替换指定位置的元素
	public void set(int index, int element) {
		// 判断下标是否越界
		if (index < 0 || index > elements.length - 1) {
			throw new RuntimeException("下标越界");
		}
		elements[index] = element;
	}
	
	//线性查找
	public int search(int target) {
		//遍历数组
		for(int i=0;i<elements.length;i++) {
			if(elements[i]==target) {
				return i;
			}
		}
		//没有找到对应的元素
		return -1;
	}
	
	//二分法查找
	public int binarySearch(int target) {
		//记录开始位置
		int begin = 0;
		//记录结束位置
		int end = elements.length-1;
		//记录中间的位置
		int mid = (begin+end)/2;
		//循环查找
		while(true) {
			//什么情况下没有这个元素?
			//开始在结束位置之后或重合,没有这个元素
			if(begin>=end) {
				return -1;
			}
			//判断中间的这个元素是不是要查找的元素
			if(elements[mid]==target) {
				return mid;
				//中间这个元素不是要查的元素
			}else{
				//判断中间这个元素是不是比目标元素大
				if(elements[mid]>target) {
					//把结束位置调整到中间位置前一个位置
					end=mid-1;
				//中间这个元素比目标元素小
				}else {
					//把开始位置调整到中间位置的后一个位置
					begin = mid+1;
				}
				//取出新的中间位置
				mid=(begin+end)/2;
			}
		}
	}

}

测试查找算法

public class TestMyArraySearch {

	public static void main(String[] args) {
		MyArray ma = new MyArray();
		ma.add(1);
		ma.add(2);
		ma.add(3);
		ma.add(4);
		ma.add(5);
		//调用线性查找方法
//		int index = ma.search(0);
//		System.out.println("index:"+index);
		//调用二分法查找
		int index2 = ma.binarySearch(6);
		System.out.println("index2:"+index2);
	}
}

测试二分查找


public class TestBinarySearch {
	
	public static void main(String[] args) {
		//目标数组
		int[] arr = new int[]{1,2,3,4,5,6,7,8,9};
		//目标元素
		int target = 3;
		//记录开始位置
		int begin = 0;
		//记录结束位置
		int end = arr.length-1;
		//记录中间的位置
		int mid = (begin+end)/2;
		//记录目标位置
		int index=-1;
		//循环查找
		while(true) {
			//判断中间的这个元素是不是要查找的元素
			if(arr[mid]==target) {
				index=mid;
				break;
			//中间这个元素不是要查的元素
			}else {
				//判断中间这个元素是不是比目标元素大
				if(arr[mid]>target) {
					//把结束位置调整到中间位置前一个位置
					end=mid-1;
				//中间这个元素比目标元素小
				}else {
					//把开始位置调整到中间位置的后一个位置
					begin = mid+1;
				}
				//取出新的中间位置
				mid=(begin+end)/2;
			}
		}
		System.out.println("index:"+index);
	}
	
}

栈和队列

public class MyStack {
	
	//栈的底层我们使用数组来存储数据
	int[] elements;

	//数组初始化
	public MyStack() {
		elements = new int[0];
	}
	
	//压入元素
	public void push(int element) {
		// 创建一个新的数组
		int[] newArr = new int[elements.length + 1];
		// 把原数组中的元素复制到新数组中
		for (int i = 0; i < elements.length; i++) {
			newArr[i] = elements[i];
		}
		// 把添加的元素放入新数组中
		newArr[elements.length] = element;
		// 使用新数组替换旧数组
		elements = newArr;
	}
	
	//取出栈顶元素
	public int pop() {
		//栈中没有元素
		if(elements.length==0) {
			throw new RuntimeException("stack is empty");
		}
		//取出数组的最后一个元素
		int element = elements[elements.length-1];
		//创建一个新的数组
		int[] newArr = new int[elements.length-1];
		//原数组中除了最后一个元素的其它元素都放入新的数组中
		for(int i=0;i<elements.length-1;i++) {
			newArr[i]=elements[i];
		}
		//替换数组
		elements=newArr;
		//返回栈顶元素
		return element;
	}
	//查看栈顶元素
	public int peek() {
		//栈中没有元素
		if(elements.length==0) {
			throw new RuntimeException("stack is empty");
		}
		return elements[elements.length-1];
	}
	//判断栈是否为空
	public boolean isEmpty() {
		return elements.length==0;
	}
}

测试

public class TestMyStack {

   public static void main(String[] args) {
      //创建一个栈
      MyStack ms = new MyStack();
      //压入数组
      ms.push(9);
      ms.push(8);
      ms.push(7);
      //最出栈顶元素
      System.out.println(ms.pop());
      System.out.println(ms.pop());
      System.out.println(ms.pop());
      //查看栈顶元素
//    System.out.println(ms.peek());
      System.out.println(ms.isEmpty());
   }

}

队列

/*队列是一种先进先出的结构*/
public class MyQueue {
	
	int[] elements;
	/*数组初始化*/
	public MyQueue() {
		elements=new int[0];
	}
	
	//入队
	public void add(int element) {
		// 创建一个新的数组
		int[] newArr = new int[elements.length + 1];
		// 把原数组中的元素复制到新数组中
		for (int i = 0; i < elements.length; i++) {
			newArr[i] = elements[i];
		}
		// 把添加的元素放入新数组中
		newArr[elements.length] = element;
		// 使用新数组替换旧数组
		elements = newArr;
	}
	
	//出队
	public int poll() {
		if (elements.length==0){
			throw new RuntimeException("queue is empty");
		}
		//把数组中的第0个元素取出来
		int element = elements[0];
		//创建一个新的数组
		int[] newArr = new int[elements.length-1];
		//复制原数组中的元素到新数组中
		for(int i=0;i<newArr.length;i++) {
			newArr[i]=elements[i+1];
		}
		//替换数组
		elements=newArr;
		return element;
	}
	//判断队列是否为空
	public boolean isEmpty() {
		return elements.length==0;
	}
}

测试

public class TestMyQueue {

   public static void main(String[] args) {
      //创建一个队列
      MyQueue mq = new MyQueue();
      //入队
      mq.add(9);
      mq.add(8);
      mq.add(7);
      //出队
      System.out.println(mq.poll());
      mq.add(6);
      System.out.println(mq.poll());
      System.out.println(mq.poll());
      System.out.println(mq.isEmpty());
      System.out.println(mq.poll());
   }
   
}

链表

单链表


//一个节点
public class Node {

	//节点内容
	int data;
	//下一个节点
	Node next;
	
	public Node(int data) {
		this.data=data;
	}
	
	//为节点追回节点
	public Node append(Node node) {
		//当前节点
		Node currentNode = this;
		//循环向后找
		while(true) {
			//取出下一个节点
			Node nextNode = currentNode.next;
			//如果下一个节点为null,当前节点已经是最后一个节点
			if(nextNode==null) {
				break;
			}
			//赋给当前节点
			currentNode = nextNode;
		}
		//把需要追回的节点追加为找到的当前节点的下一个节点
		currentNode.next=node;
		return this;
	}
	
	//插入一个节点做为当前节点的下一个节点
	public void after(Node node) {
		//取出下一个节点,作为下下一个节点
		Node nextNext = next;
		//把新节点作为当前节点的下一个节点
		this.next=node;
		//把下下一个节点设置为新节点的下一个节点
		node.next=nextNext;
	}
	
	//显示所有节点信息
	public void show() {
		Node currentNode = this;
		while(true) {
			System.out.print(currentNode.data+" ");
			//取出下一个节点
			currentNode=currentNode.next;
			//如果是最后一个节点
			if(currentNode==null) {
				break;
			}
		}
		System.out.println();
	}
	
	//删除下一个节点
	public void removeNext() {
		//取出下下一个节点
		Node newNext = this.next.next;
		//把下下一个节点设置为当前节点的下一个节点。
		this.next=newNext;
	}
	
	//获取下一个节点
	public Node next() {
		return this.next;
	}
	
	//获取节点中的数据
	public int getData() {
		return this.data;
	}
	
	//当前节点是否是最后一个节点
	public boolean isLast() {
		return next==null;
	}
	
}

测试

public class TestNode {
   
   public static void main(String[] args) {
      //创建节点
      Node n1 = new Node(1);
      Node n2 = new Node(2);
      Node n3 = new Node(3);
      //追加节点
      n1.append(n2).append(n3).append(new Node(4));
      //取出下一个节点的数据
//    System.out.println(n1.next().next().next().getData());
      //判断节点是否为最后一个节点
//    System.out.println(n1.isLast());
//    System.out.println(n1.next().next().next().isLast());
      
      //显示所有节点内容
      n1.show();
      //删除一个节点
//    n1.next().removeNext();
      //显示所有节点内容
//    n1.show();
      //插入一个新节点
      Node node = new Node(5);
      n1.next().after(node);
      n1.show();
   }

}

循环列表

image-20200526153122125

image-20200526153135851

//一个节点
public class LoopNode {

   //节点内容
   int data;
   //下一个节点(默认值是自己)
   LoopNode next=this;//与自己相连
   
   public LoopNode(int data) {
      this.data=data;
   }
   
   //插入一个节点做为当前节点的下一个节点
   public void after(LoopNode node) {
      //取出下一个节点,作为下下一个节点
      LoopNode nextNext = next;
      //把新节点作为当前节点的下一个节点
      this.next=node;
      //把下下一个节点设置为新节点的下一个节点
      node.next=nextNext;
   }
   
   //删除下一个节点
   public void removeNext() {
      //取出下下一个节点
      LoopNode newNext = next.next;
      //把下下一个节点设置为当前节点的下一个节点。
      this.next=newNext;
   }
   
   //获取下一个节点
   public LoopNode next() {
      return this.next;
   }
   
   //获取节点中的数据
   public int getData() {
      return this.data;
   }
   
}

测试

public class TestLoopNode {
   public static void main(String[] args) {
      /*每个链表都与自己相连*/
      LoopNode n1 = new LoopNode(1);
      LoopNode n2 = new LoopNode(2);
      LoopNode n3 = new LoopNode(3);
      LoopNode n4 = new LoopNode(4);
      //增加节点
      n1.after(n2);//1-2-1
      n2.after(n3);//1-2-3-1
      n3.after(n4);//1-2-3-4-1
      System.out.println(n1.next().getData());
      System.out.println(n2.next().getData());
      System.out.println(n3.next().getData());
      System.out.println(n4.next().getData());
   }
}

双链表


public class DoubleNode {
	//上一个节点
	DoubleNode pre=this;
	//下一个节点
	DoubleNode next=this;
	//节点数据
	int data;
	
	public DoubleNode(int data) {
		this.data=data;
	}
	
	//增节点
	public void after(DoubleNode node) {
		//原来的下一个节点
		DoubleNode nextNext = next;
		//把新节点做为当前节点的下一个节点
		this.next=node;
		//把当前节点做新节点的前一个节点
		node.pre=this;
		//让原来的下一个节点作新节点的下一个节点
		node.next=nextNext;
		//让原来的下一个节点的上一个节点为新节点
		nextNext.pre=node;
	}
	//下一个节点
	public DoubleNode next() {
		return this.next;
	}
	//上一个节点
	public DoubleNode pre() {
		return this.pre;
	}
	//获取数据
	public int getData() {
		return this.data;
	}
}

测试

public class TestDoubleNode {

   public static void main(String[] args) {
      //创建节点
      DoubleNode n1 = new DoubleNode(1);
      DoubleNode n2 = new DoubleNode(2);
      DoubleNode n3 = new DoubleNode(3);
      //追加节点
      n1.after(n2);
      n2.after(n3);
      //查看上一个,自己,下一个节点的内容
      System.out.println(n2.pre().getData());
      System.out.println(n2.getData());
      System.out.println(n2.next().getData());
      System.out.println(n3.next().getData());
      System.out.println(n1.pre().getData()); 
   } 
}

递归

递归:在一个方法(函数)的内部调用该方法(函数)本身的编程方式

public class TestRecursive {

   public static void main(String[] args) {
      print(3);
   }
   
   //递归
   public static void print(int i) {
      if(i>0) {
         System.out.println(i);
         print(i-1);
      }
   }

}

斐波那契数列

public class TestFebonacci {

   public static void main(String[] args) {
      //斐波那契数列:1 1 2 3 5 8 13
      int i = febonacci(7);
      System.out.println(i);
   }
   
   //打印第n项斐波那契数列
   public static int febonacci(int i) {
      if(i==1 || i==2) {
         return 1;
      }else {
         return febonacci(i-1)+febonacci(i-2);
      }
   }

}

汉诺塔问题

public class TestHanoi {

	public static void main(String[] args) {
		hanoi(5,'A','B','C');
	}
	
	/**
	 * @param n 	共有N个盘子
	 * @param from	开始的柱子
	 * @param in		中间的柱子
	 * @param to		目标柱子
	 * 无论有多少个盘子,都认为只有两个。上面的所有盘子和最下面一个盘子。
	 */
	public static void hanoi(int n,char from,char in,char to) {
		//只有一个盘子。
		if(n==1) {
			System.out.println("第1个盘子从"+from+"移到"+to);
		//无论有多少个盘子,都认为只有两个。上面的所有盘子和最下面一个盘子。
		}else {
			//移动上面所有的盘子到中间位置
			hanoi(n-1,from,to,in);
			//移动下面的盘子
			System.out.println("第"+n+"个盘子从"+from+"移到"+to);
			//把上面的所有盘子从中间位置移到目标位置
			hanoi(n-1,in,from,to);
		}
	}

}

二叉树

二叉树的链式存储

package basic.b05tree;

/*链式二叉树的结点*/
public class Node {
	//节点的权
	int value;
	//左儿子
	Node leftNode;
	//右儿子
	Node rightNode;
	
	public Node(int value) {
		this.value=value;
	}
	
	//设置左儿子
	public void setLeftNode(Node leftNode) {
		this.leftNode = leftNode;
	}
	//设置右儿子
	public void setRightNode(Node rightNode) {
		this.rightNode = rightNode;
	}
	
	//前序遍历
	public void frontShow() {
		//先遍历当前节点的内容
		System.out.println(value);
		//左节点
		if(leftNode!=null) {
			leftNode.frontShow();
		}
		//右节点
		if(rightNode!=null) {
			rightNode.frontShow();
		}
	}

	//中序遍历
	public void midShow() {
		//左子节点
		if(leftNode!=null) {
			leftNode.midShow();
		}
		//当前节点
		System.out.println(value);
		//右子节点
		if(rightNode!=null) {
			rightNode.midShow();
		}
	}

	//后序遍历
	public void afterShow() {
		//左子节点
		if(leftNode!=null) {
			leftNode.afterShow();
		}
		//右子节点
		if(rightNode!=null) {
			rightNode.afterShow();
		}
		//当前节点
		System.out.println(value);
	}

	//前序查找
	public Node frontSearch(int i) {
		Node target=null;
		//对比当前节点的值
		if(this.value==i) {
			return this;
		//当前节点的值不是要查找的节点
		}else {
			//查找左儿子
			if(leftNode!=null) {
				//有可能可以查到,也可以查不到,查不到的话,target还是一个null
				target = leftNode.frontSearch(i);
			}
			//如果不为空,说明在左儿子中已经找到
			if(target!=null) {
				return target;
			}
			//查找右儿子
			if(rightNode!=null) {
				target=rightNode.frontSearch(i);
			}
		}
		return target;
	}
	
	//删除一个子树
	public void delete(int i) {
		Node parent = this;
		//判断左儿子
		if(parent.leftNode!=null&&parent.leftNode.value==i) {
			parent.leftNode=null;
			return;
		}
		//判断右儿子
		if(parent.rightNode!=null&&parent.rightNode.value==i) {
			parent.rightNode=null;
			return;
		}
		
		//递归检查并删除左儿子
		parent=leftNode;
		if(parent!=null) {
			parent.delete(i);
		}
		
		//递归检查并删除右儿子
		parent=rightNode;
		if(parent!=null) {
			parent.delete(i);
		}
	}

}

package basic.b05tree;

/*链式二叉树*/
public class BinaryTree {

	Node root;
	
	//设置根节点
	public void setRoot(Node root) {
		this.root = root;
	}
	
	//获取根节点
	public Node getRoot() {
		return root;
	}

	public void frontShow() {
		if(root!=null) {
			root.frontShow();
		}
	}

	public void midShow() {
		if(root!=null) {
			root.midShow();
		}
	}

	public void afterShow() {
		if(root!=null) {
			root.afterShow();
		}
	}

	public Node frontSearch(int i) {
		return root.frontSearch(i);
	}

	public void delete(int i) {
		if(root.value==i) {
			root=null;
		}else {
			root.delete(i);
		}
	}
	
}

package basic.b05tree;

/*链式存储的二叉树*/
public class TestBinaryTree {

	public static void main(String[] args) {
		//创建一颗树
		BinaryTree binTree = new BinaryTree();
		//创建一个根节点
		Node root = new Node(1);
		//把根节点赋给树
		binTree.setRoot(root);
		//创建一个左节点
		Node rootL = new Node(2);
		//把新创建的节点设置为根节点的子节点
		root.setLeftNode(rootL);
		//创建一个右节点
		Node rootR = new Node(3);
		//把新创建的节点设置为根节点的子节点
		root.setRightNode(rootR);
		//为第二层的左节点创建两个子节点
		rootL.setLeftNode(new Node(4));
		rootL.setRightNode(new Node(5));
		//为第二层的右节点创建两个子节点
		rootR.setLeftNode(new Node(6));
		rootR.setRightNode(new Node(7));
		//前序遍历树
		binTree.frontShow();
		System.out.println("===============");
		//中序遍历
		binTree.midShow();
		System.out.println("===============");
		//后序遍历
		binTree.afterShow();
		System.out.println("===============");
		//前序查找
		Node result = binTree.frontSearch(5);
		System.out.println(result);
		
		System.out.println("===============");
		//删除一个子树
		binTree.delete(4);
		binTree.frontShow();
	}
}

二叉树的顺序存储

image-20200526222739868

package basic.b06binarytree;

public class ArrayBinaryTree {

	int[] data;
	
	public ArrayBinaryTree(int[] data) {
		this.data=data;
	}
	
	public void frontShow() {
		frontShow(0);
	}
	
	//前序遍历
	public void frontShow(int index) {
		if(data==null||data.length==0) {
			return;
		}
		//先遍历当前节点的内容
		System.out.println(data[index]);
		//2*index+1:处理左子树
		if(2*index+1<data.length) {
			frontShow(2*index+1);
		}
		//2*index+2:处理右子树
		if(2*index+2<data.length) {
			frontShow(2*index+2);
		}
	}
	
}

package basic.b06binarytree;

/*顺序存储的二叉树(完全二叉树)*/
public class TestArrayBinaryTree {

	public static void main(String[] args) {
		int[] data = new int[] {1,2,3,4,5,6,7};
		ArrayBinaryTree tree = new ArrayBinaryTree(data);
		//前序遍历
		tree.frontShow();
	}
}

线索二叉树

中序线索二叉树

image-20200526223931975

线索化二叉树时,一个节点的前一个节点,叫前驱节点

线索化二叉树时,一个节点的后一个节点,叫后继节点

package basic.demo7;

public class ThreadedNode {
	//节点的权
	int value;
	//左儿子
	ThreadedNode leftNode;
	//右儿子
	ThreadedNode rightNode;
	//标识指针类型
	int leftType;
	int rightType;
	

	public ThreadedNode(int value) {
		this.value=value;
	}
	
	//设置左儿子
	public void setLeftNode(ThreadedNode leftNode) {
		this.leftNode = leftNode;
	}
	//设置右儿子
	public void setRightNode(ThreadedNode rightNode) {
		this.rightNode = rightNode;
	}
	
	//前序遍历
	public void frontShow() {
		//先遍历当前节点的内容
		System.out.println(value);
		//左节点
		if(leftNode!=null) {
			leftNode.frontShow();
		}
		//右节点
		if(rightNode!=null) {
			rightNode.frontShow();
		}
	}

	//中序遍历
	public void midShow() {
		//左子节点
		if(leftNode!=null) {
			leftNode.midShow();
		}
		//当前节点
		System.out.println(value);
		//右子节点
		if(rightNode!=null) {
			rightNode.midShow();
		}
	}

	//后序遍历
	public void afterShow() {
		//左子节点
		if(leftNode!=null) {
			leftNode.afterShow();
		}
		//右子节点
		if(rightNode!=null) {
			rightNode.afterShow();
		}
		//当前节点
		System.out.println(value);
	}

	//前序查找
	public ThreadedNode frontSearch(int i) {
		ThreadedNode target=null;
		//对比当前节点的值
		if(this.value==i) {
			return this;
		//当前节点的值不是要查找的节点
		}else {
			//查找左儿子
			if(leftNode!=null) {
				//有可能可以查到,也可以查不到,查不到的话,target还是一个null
				target = leftNode.frontSearch(i);
			}
			//如果不为空,说明在左儿子中已经找到
			if(target!=null) {
				return target;
			}
			//查找右儿子
			if(rightNode!=null) {
				target=rightNode.frontSearch(i);
			}
		}
		return target;
	}
	
	//删除一个子树
	public void delete(int i) {
		ThreadedNode parent = this;
		//判断左儿子
		if(parent.leftNode!=null&&parent.leftNode.value==i) {
			parent.leftNode=null;
			return;
		}
		//判断右儿子
		if(parent.rightNode!=null&&parent.rightNode.value==i) {
			parent.rightNode=null;
			return;
		}
		
		//递归检查并删除左儿子
		parent=leftNode;
		if(parent!=null) {
			parent.delete(i);
		}
		
		//递归检查并删除右儿子
		parent=rightNode;
		if(parent!=null) {
			parent.delete(i);
		}
	}
}
package basic.demo7;
/*线索二叉树*/
public class ThreadedBinaryTree {

	ThreadedNode root;
	//用于临时存储前驱节点
	ThreadedNode pre=null;
	
	//遍历线索二叉树
	public void threadIterate() {
		//用于临时存储当前遍历节点
		ThreadedNode node = root;
		while(node!=null) {
			//循环找到最开始的节点
			while(node.leftType==0) {
				node=node.leftNode;
			}
			//打印当前节点的值
			System.out.println(node.value);
			//如果当前节点的右指针指向的是后继节点,可能后继节点还有后继节点、
			while(node.rightType==1) {
				node=node.rightNode;
				System.out.println(node.value);
			}
			//替换遍历的节点
			node=node.rightNode;
		}
	}
	
	//设置根节点
	public void setRoot(ThreadedNode root) {
		this.root = root;
	}
	
	//中序线索化二叉树
	public void threadNodes() {
		threadNodes(root);
	}
	
	public void threadNodes(ThreadedNode node) {
		//当前节点如果为null,直接返回
		if(node==null) {
			return;
		}
		//处理左子树
		threadNodes(node.leftNode);
		//处理前驱节点
		if(node.leftNode==null){
			//让当前节点的左指针指向前驱节点
			node.leftNode=pre;
			//改变当前节点左指针的类型
			node.leftType=1;
		}
		//处理前驱的右指针,如果前驱节点的右指针是null(没有指下右子树)
		if(pre!=null&&pre.rightNode==null) {
			//让前驱节点的右指针指向当前节点
			pre.rightNode=node;
			//改变前驱节点的右指针类型
			pre.rightType=1;
		}
		//每处理一个节点,当前节点是下一个节点的前驱节点
		pre=node;
		//处理右子树
		threadNodes(node.rightNode);
	}
	
	//获取根节点
	public ThreadedNode getRoot() {
		return root;
	}

	//前序遍历
	public void frontShow() {
		if(root!=null) {
			root.frontShow();
		}
	}

	//中序遍历
	public void midShow() {
		if(root!=null) {
			root.midShow();
		}
	}

	//后序遍历
	public void afterShow() {
		if(root!=null) {
			root.afterShow();
		}
	}

	//前序查找
	public ThreadedNode frontSearch(int i) {
		return root.frontSearch(i);
	}

	//删除子树
	public void delete(int i) {
		if(root.value==i) {
			root=null;
		}else {
			root.delete(i);
		}
	}
	
}

package basic.demo7;

public class TestThreadedBinaryTree {

	public static void main(String[] args) {
		//创建一颗树
		ThreadedBinaryTree binTree = new ThreadedBinaryTree();
		//创建一个根节点
		ThreadedNode root = new ThreadedNode(1);
		//把根节点赋给树
		binTree.setRoot(root);
		//创建一个左节点
		ThreadedNode rootL = new ThreadedNode(2);
		//把新创建的节点设置为根节点的子节点
		root.setLeftNode(rootL);
		//创建一个右节点
		ThreadedNode rootR = new ThreadedNode(3);
		//把新创建的节点设置为根节点的子节点
		root.setRightNode(rootR);
		//为第二层的左节点创建两个子节点
		rootL.setLeftNode(new ThreadedNode(4));
		ThreadedNode fiveNode = new ThreadedNode(5);
		rootL.setRightNode(fiveNode);
		//为第二层的右节点创建两个子节点
		rootR.setLeftNode(new ThreadedNode(6));
		rootR.setRightNode(new ThreadedNode(7));
		//中序遍历树
		binTree.midShow();
		System.out.println("===============");
		//中前线索化二叉树
		binTree.threadNodes();
		binTree.threadIterate();
	}

}

取出根节点权值最小的两颗二叉树

组成一颗新的二叉树,前面取出来的两颗二叉树是新二叉的两个子树

根节点的权值是前两取出来的两颗二叉树的根节点的权值之和

创建赫夫曼树

package basic.demo9;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

public class TestHuffmanTree {
	public static void main(String[] args) {
		int[] arr = {3,7,8,29,5,11,23,14};
		Node node = createHuffmanTree(arr);//获得一棵树的根结点就相当于获取了整棵树
		System.out.println(node);
	}
	
	//创建赫夫曼树
	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 left = nodes.get(nodes.size()-1);
			//取出最权值次小的二叉树
			Node right = nodes.get(nodes.size()-2);
			//创建一颗新的二叉树
			Node parent = new Node(left.value+right.value);
			//把取出来的两个二叉树移除
			nodes.remove(left);
			nodes.remove(right);
			//放入原来的二叉树集合中
			nodes.add(parent);
		}
		return nodes.get(0);
	}
	
}

package basic.demo9;

public 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);
	}

	@Override
	public String toString() {
		return "Node [value=" + value + "]";
	}
}

赫夫曼编码代码实现

image-20200527164731233

  1. 统计字符数并排序
  2. 创建赫夫晏树
  3. 创建赫夫旻编码表
  4. 编码

    public class Node implements Comparable<Node> {
    	Byte data;
    	int weight;
    	Node left;
    	Node right;
    	public Node(Byte data,int weight) {
    		this.data=data;
    		this.weight=weight;
    	}
    	
    	@Override
    	public String toString() {
    		return "Node [data=" + data + ", weight=" + weight + "]";
    	}
    
    	@Override
    	public int compareTo(Node o) {
    		return o.weight-this.weight;
    	}
    }
    
    package basic.demo10;
    
    import java.io.FileInputStream;
    import java.io.FileNotFoundException;
    import java.io.FileOutputStream;
    import java.io.IOException;
    import java.io.InputStream;
    import java.io.ObjectInputStream;
    import java.io.ObjectOutputStream;
    import java.io.OutputStream;
    import java.util.ArrayList;
    import java.util.Arrays;
    import java.util.Collections;
    import java.util.HashMap;
    import java.util.List;
    import java.util.Map;
    import java.util.Map.Entry;
    public class TestHuffmanCode {
    
    	public static void main(String[] args) {
    //		String msg="can you can a can as a can canner can a can.";
    //		byte[] bytes = msg.getBytes();
    //		//进行赫夫曼编码压缩
    //		byte[] b = huffmanZip(bytes);
    //		//使用赫夫曼编码进行解码
    //		byte[] newBytes = decode(huffCodes,b);
    //		System.out.println(new String(newBytes));
    		String src="1.bmp";
    		String dst="2.zip";
    //		try {
    //			zipFile(src, dst);
    //		} catch (IOException e) {
    //			e.printStackTrace();
    //		}
    		try {
    			unZip("2.zip", "3.bmp");
    		} catch (Exception e) {
    			e.printStackTrace();
    		}
    	}
    	
    	/**
    	 * 文件的解压
    	 * @param src
    	 * @param dst
    	 * @throws Exception 
    	 */
    	public static void unZip(String src,String dst) throws Exception {
    		//创建一个输入流
    		InputStream is  = new FileInputStream("2.zip");
    		ObjectInputStream ois = new ObjectInputStream(is);
    		//读取byte数组
    		byte[] b = (byte[]) ois.readObject();
    		//读取赫夫曼编码表
    		Map<Byte, String> codes = (Map<Byte, String>) ois.readObject();
    		ois.close();
    		is.close();
    		//解码
    		byte[] bytes = decode(codes, b);
    		//创建一个输出流
    		OutputStream os  = new FileOutputStream(dst);
    		//写出数据
    		os.write(bytes);
    		os.close();
    	}
    	
    	/**
    	 * 压缩文件
    	 * @param src
    	 * @param dst
    	 * @throws IOException
    	 */
    	public static void zipFile(String src,String dst) throws IOException {
    		//创建一个输入流
    		InputStream is = new FileInputStream(src);
    		//创建一个和输入流指向的文件大小一样的byte数组
    		byte[] b = new byte[is.available()];
    		//读取文件内容
    		is.read(b);
    		is.close();
    		//使用赫夫曼编码进行编码
    		byte[] byteZip = huffmanZip(b);
    		//输出流
    		OutputStream os = new FileOutputStream(dst);
    		ObjectOutputStream oos = new ObjectOutputStream(os);
    		//把压缩后的byte数组写入文件
    		oos.writeObject(byteZip);
    		//把赫夫曼编码表写入文件
    		oos.writeObject(huffCodes);
    		oos.close();
    		os.close();
    	}
    	
    	/**
    	 * 使用指定的赫夫曼编码表进行解码
    	 * @param huffCodes2
    	 * @param b
    	 * @return
    	 */
    	private static byte[] decode(Map<Byte, String> huffCodes, byte[] bytes) {
    		StringBuilder sb = new StringBuilder();
    		//把byte数组转为一个二进制的字符串
    		for(int i=0;i<bytes.length;i++) {
    			byte b = bytes[i];
    			//是否是最后一个。
    			boolean flag = (i==bytes.length-1);
    			sb.append(byteToBitStr(!flag,b));
    		}
    		//把字符串按照指定的赫夫曼编码进行解码
    		//把赫夫曼编码的键值对进行调换
    		Map<String, Byte> map = new HashMap<>();
    		for(Entry<Byte, String> entry:huffCodes.entrySet()) {
    			map.put(entry.getValue(), entry.getKey());
    		}
    		//创建一个集合,用于存byte
    		List<Byte> list = new ArrayList<>();
    		//处理字符串
    		for(int i=0;i<sb.length();) {
    			int count=1;
    			boolean flag = true;
    			Byte b=null;
    			//截取出一个byte
    			while(flag) {
    				String key = sb.substring(i, i+count);
    				b = map.get(key);
    				if(b==null) {
    					count++;
    				}else {
    					flag=false;
    				}
    			}
    			list.add(b);
    			i+=count;
    		}
    		//把集合转为数组
    		byte[] b = new byte[list.size()];
    		for(int i=0;i<b.length;i++) {
    			b[i]=list.get(i);
    		}
    		return b;
    	}
    	
    	private static String byteToBitStr(boolean flag,byte b) {
    		int temp=b;
    		if(flag) {
    			temp|=256;
    		}
    		String str = Integer.toBinaryString(temp);
    		if(flag) {
    			return str.substring(str.length()-8);
    		}else {
    			return str;
    		}
    	}
    
    	/**
    	 * 进行赫夫曼编码压缩的方法
    	 * @param bytes
    	 * @return
    	 */
    	private static byte[] huffmanZip(byte[] bytes) {
    		//先统计每一个byte出现的次数,并放入一个集合中
    		List<Node> nodes = getNodes(bytes);
    		//创建一颗赫夫曼树
    		Node tree = createHuffmanTree(nodes);
    		//创建一个赫夫曼编码表
    		Map<Byte, String> huffCodes = getCodes(tree);
    		//编码
    		byte[] b = zip(bytes,huffCodes);
    		return b;
    	}
    	
    	/**
    	 * 进行赫夫曼编码
    	 * @param bytes
    	 * @param huffCodes2
    	 * @return
    	 */
    	private static byte[] zip(byte[] bytes, Map<Byte, String> huffCodes) {
    		StringBuilder sb = new StringBuilder();
    		//把需要压缩的byte数组处理成一个二进制的字符串
    		for(byte b:bytes) {
    			sb.append(huffCodes.get(b));
    		}
    		//定义长度
    		int len;
    		if(sb.length()%8==0) {
    			len=sb.length()/8;
    		}else {
    			len=sb.length()/8+1;
    		}
    		//用于存储压缩后的byte
    		byte[] by = new byte[len];
    		//记录新byte的位置
    		int index = 0;
    		for(int i=0;i<sb.length();i+=8) {
    			String strByte;
    			if(i+8>sb.length()) {
    				strByte = sb.substring(i);
    			}else {
    				strByte = sb.substring(i, i+8);
    			}
    			byte byt = (byte)Integer.parseInt(strByte, 2);
    			by[index]=byt;
    			index++;
    		}
    		return by;
    	}
    
    	//用于临时存储路径
    	static StringBuilder sb = new StringBuilder();
    	//用于存储赫夫曼编码
    	static Map<Byte, String> huffCodes = new HashMap<>();
    	/**
    	 * 根据赫夫曼树获取赫夫曼编码
    	 * @param tree
    	 * @return
    	 */
    	private static Map<Byte, String> getCodes(Node tree) {
    		if(tree==null) {
    			return null;
    		}
    		getCodes(tree.left,"0",sb);
    		getCodes(tree.right,"1",sb);
    		return huffCodes;
    	}
    
    	private static void getCodes(Node node, String code, StringBuilder sb) {
    		StringBuilder sb2 = new StringBuilder(sb);
    		sb2.append(code);
    		if(node.data==null) {
    			getCodes(node.left, "0", sb2);
    			getCodes(node.right, "1", sb2);
    		}else {
    			huffCodes.put(node.data, sb2.toString());
    		}
    	}
    
    	/**
    	 * 创建赫夫曼树
    	 * @param nodes
    	 * @return
    	 */
    	private static Node createHuffmanTree(List<Node> nodes) {
    		while(nodes.size()>1) {
    			//排序
    			Collections.sort(nodes);
    			//取出两个权值最低的二叉树
    			Node left = nodes.get(nodes.size()-1);
    			Node right = nodes.get(nodes.size()-2);
    			//创建一颗新的二叉树
    			Node parent = new Node(null, left.weight+right.weight);
    			//把之前取出来的两颗二叉树设置为新创建的二叉树的子树
    			parent.left=left;
    			parent.right=right;
    			//把前面取出来的两颗二叉树删除
    			nodes.remove(left);
    			nodes.remove(right);
    			//把新创建的二叉树放入集合中
    			nodes.add(parent);
    		}
    		return nodes.get(0);
    	}
    
    	/**
    	 * 把byte数组转为node集合
    	 * @param bytes
    	 * @return
    	 */
    	private static List<Node> getNodes(byte[] bytes) {
    		List<Node> nodes = new ArrayList<>();
    		//存储每一个byte出现了多少次。
    		Map<Byte, Integer> counts = new HashMap<>();
    		//统计每一个byte出现的次数
    		for(byte b:bytes) {
    			Integer count = counts.get(b);
    			if(count==null) {
    				counts.put(b, 1);
    			}else {
    				counts.put(b, count+1);
    			}
    		}
    		//把每一个键值对转为一个node对象
    		for(Entry<Byte, Integer> entry:counts.entrySet()) {
    			nodes.add(new Node(entry.getKey(), entry.getValue()));
    		}
    		return nodes;
    	}
    
    }
    

二叉排序树

线性结构

顺序存储,不排序:查找困难 顺序存储,排序:删除插入困难

链式存储:无论是否排序 查找困难

image-20200527164646134

什么是二叉排序树,也叫二叉查找树,二叉搜索树: BST对于二叉树中的任何-个非叶子节点,要求左子节点比当前节点值小,右子节点比当前节点值大。(空树也是一棵二叉排序树)

package basic.demo11;

public class Node {
	int value;
	Node left;
	Node right;
	
	public Node(int value) {
		this.value=value;
	}

	/**
	 * 向子树中添加节点
	 * @param node
	 */
	public void add(Node node) {
		if(node==null) {
			return;
		}
		//判断传入的节点的值比当前子树的根节点的值大还是小
		//添加的节点比当前节点的值更小
		if(node.value<this.value) {
			//如果左节点为空
			if(this.left==null) {
				this.left=node;
			//如果不为空
			}else {
				this.left.add(node);
			}
		}else {
			if(this.right==null) {
				this.right=node;
			}else {
				this.right.add(node);
			}
		}
	}

	/**
	 * 中序遍历
	 * @param node
	 */
	public void midShow(Node node) {
		if(node==null) {
			return;
		}
		midShow(node.left);
		System.out.println(node.value);
		midShow(node.right);
	}

	/**
	 * 查找节点
	 * @param value2
	 */
	public Node search(int value) {
		if(this.value==value) {
			return this;
		}else if(value<this.value) {
			if(left==null) {
				return null;
			}
			return left.search(value);
		}else{
			if(right==null) {
				return null;
			}
			return right.search(value);
		}
	}

	/**
	 * 搜索父节点
	 * @param value
	 * @return
	 */
	public Node searchParent(int value) {
		if((this.left!=null&&this.left.value==value)||(this.right!=null&&this.right.value==value)) {
			return this;
		}else {
			if(this.value>value&&this.left!=null) {
				return this.left.searchParent(value);
			}else if(this.value<value&&this.right!=null){
				return this.right.searchParent(value);
			}
			return null;
		}
	}
}

package basic.demo11;

public class BinarySortTree {
	Node root;
	
	/**
	 * 向二叉排序树中添加节点
	 * @param node
	 */
	public void add(Node node){
		//如果是一颗空树
		if(root==null) {
			root=node;
		}else {
			root.add(node);
		}
	}
	
	/**
	 * 中序遍历二叉排序树,从小到大的顺序
	 */
	public void midShow() {
		if(root!=null) {
			root.midShow(root);
		}
	}
	
	/**
	 * 节点的查找
	 * @param value
	 * @return
	 */
	public Node search(int value) {
		if(root==null) {
			return null;
		}else {
			return root.search(value);
		}
	}
	
	/**
	 * 删除节点
	 * @param value
	 */
	public void delete(int value) {
		if(root==null) {
			return;
		}else {
			//找到这个节点
			Node target = search(value);
			//如果没有这个节点
			if(target==null) {
				return;
			}
			//找到他的父节点
			Node parent = searchParent(value);
			//要删除的节点是叶子节点
			if(target.left==null&&target.right==null) {
				//要删除的节点是父节点的左子节点
				if(parent.left.value==value) {
					parent.left=null;
					//要删除的节点是父节点的右子节点
				}else {
					parent.right=null;
				}
			//要删除的节点有两个子节点的情况
			}else if(target.left!=null&&target.right!=null) {
				//删除右子树中值最小的节点,取获取到该节点的值
				int min = deleteMin(target.right);
				//替换目标节点中的值
				target.value=min;
			//要删除的节点有一个左子节点或右子节点
			}else {
				//有左子节点
				if(target.left!=null) {
					//要删除的节点是父节点的左子节点
					if(parent.left.value==value) {
						parent.left=target.left;
						//要删除的节点是父节点的右子节点
					}else {
						parent.right=target.left;
					}
				//有右子节点
				}else {
					//要删除的节点是父节点的左子节点
					if(parent.left.value==value) {
						parent.left=target.right;
						//要删除的节点是父节点的右子节点
					}else {
						parent.right=target.right;
					}
				}
			}
		}
	}
	
	/**
	 * 删除一颗树中最小的节点
	 * @param right
	 * @return
	 */
	private int deleteMin(Node node) {
		Node target = node;
		//递归向左找
		while(target.left!=null) {
			target=target.left;
		}
		//删除最小的这个节点
		delete(target.value);
		return target.value;
	}

	/**
	 * 搜索父节点
	 * @param value
	 * @return
	 */
	public Node searchParent(int value) {
		if(root==null) {
			return null;
		}else {
			return root.searchParent(value);
		}
	}
}

package basic.demo11;

public class TestBinarySortTree {

	public static void main(String[] args) {
		int[] arr = new int[] {7,3,10,12,5,1,9};
		//创建一颗二叉排序树
		BinarySortTree bst = new BinarySortTree();
		//循环添加
		for(int i:arr) {
			bst.add(new Node(i));
		}
		//查看树中的值
		bst.midShow();
		System.out.println("-----");
		//查找
//		Node node = bst.search(10);
//		System.out.println(node.value);
		//
//		Node node2 = bst.search(20);
//		System.out.println(node2);
//		//测试查找父节点
//		Node p1 = bst.searchParent(12);
//		System.out.println(p1.value);
//		System.out.println("-----");
		//删除叶子节点
//		bst.delete(5);
//		bst.midShow();
//		System.out.println("===");
		//删除只有一个子节点的节点
//		bst.delete(3);
//		bst.midShow();
		//删除有两个子节点的节点
		bst.delete(3);
		System.out.println("----");
		bst.midShow();
		
		
	}

}

缺点:数据为1、2、3、4、5、6、7、8、9时结构就很差

AVL树

对于任何左子树和右子树——的高度差的绝对值不超过1.

image-20200527150637920

单旋转

左左:右旋

image-20200527152839684

右右与左左类似需要左旋

image-20200527153622495

双旋转

image-20200527154917215

实现

package basic.demo12;

public class Node {
	int value;
	Node left;
	Node right;
	
	public Node(int value) {
		this.value=value;
	}

	/**
	 * 返回当前节点的高度
	 * @return
	 */
	public int height() {
		return Math.max(left==null?0:left.height(), right==null?0:right.height())+1;
	}
	
	/**
	 * 获取左子树的高度
	 * @return
	 */
	public int leftHeight() {
		if(left==null) {
			return 0;
		}
		return left.height();
	}
	
	/**
	 * 获取右子树的高度
	 * @return
	 */
	public int rightHeight() {
		if(right==null) {
			return 0;
		}
		return right.height();
	}

	/**
	 * 向子树中添加节点
	 * @param node
	 */
	public void add(Node node) {
		if(node==null) {
			return;
		}
		//判断传入的节点的值比当前子树的根节点的值大还是小
		//添加的节点比当前节点的值更小
		if(node.value<this.value) {
			//如果左节点为空
			if(this.left==null) {
				this.left=node;
			//如果不为空
			}else {
				this.left.add(node);
			}
		}else {
			if(this.right==null) {
				this.right=node;
			}else {
				this.right.add(node);
			}
		}
		//查询是否平衡
		//进行右旋转
		if(leftHeight()-rightHeight()>=2) {
			//双旋转
			if(left!=null&&left.leftHeight()<left.rightHeight()) {
				//先左旋转
				left.leftRotate();
				//再右旋转
				rightRotate();
			//单旋转
			}else {
				rightRotate();
			}
		}
		//左旋转
		if(leftHeight()-rightHeight()<=-2) {
			//双旋转
			if(right!=null&&right.rightHeight()<right.leftHeight()) {
				right.rightRotate();
				leftRotate();
			//单旋转
			}else {
				leftRotate();
			}
		}
	}
	
	/**
	 * 左旋转
	 */
	private void leftRotate() {
		Node newLeft = new Node(value);
		newLeft.left=left;
		newLeft.right=right.left;
		value=right.value;
		right=right.right;
		left=newLeft;
	}

	/**
	 * 右旋转
	 */
	private void rightRotate() {
		//创建一个新的节点,值等于当前节点的值
		Node newRight = new Node(value);
		//把新节点的右子树设置了当前节点的右子树
		newRight.right=right;
		//把新节点的左子树设置为当前节点的左子树的右子树
		newRight.left=left.right;
		//把当前节点的值换为左子节点的值
		value=left.value;
		//把当前节点的左子树设置了左子树的左子树
		left=left.left;
		//把当前节点的右子树设置为新节点
		right=newRight;
	}

	/**
	 * 中序遍历
	 * @param node
	 */
	public void midShow(Node node) {
		if(node==null) {
			return;
		}
		midShow(node.left);
		System.out.println(node.value);
		midShow(node.right);
	}

	/**
	 * 查找节点
	 * @param value2
	 */
	public Node search(int value) {
		if(this.value==value) {
			return this;
		}else if(value<this.value) {
			if(left==null) {
				return null;
			}
			return left.search(value);
		}else{
			if(right==null) {
				return null;
			}
			return right.search(value);
		}
	}

	/**
	 * 搜索父节点
	 * @param value
	 * @return
	 */
	public Node searchParent(int value) {
		if((this.left!=null&&this.left.value==value)||(this.right!=null&&this.right.value==value)) {
			return this;
		}else {
			if(this.value>value&&this.left!=null) {
				return this.left.searchParent(value);
			}else if(this.value<value&&this.right!=null){
				return this.right.searchParent(value);
			}
			return null;
		}
	}
}

package basic.demo12;

public class BinarySortTree {
	Node root;
	
	/**
	 * 向二叉排序树中添加节点
	 * @param node
	 */
	public void add(Node node){
		//如果是一颗空树
		if(root==null) {
			root=node;
		}else {
			root.add(node);
		}
	}
	
	/**
	 * 中序遍历二叉排序树,从小到大的顺序
	 */
	public void midShow() {
		if(root!=null) {
			root.midShow(root);
		}
	}
	
	/**
	 * 节点的查找
	 * @param value
	 * @return
	 */
	public Node search(int value) {
		if(root==null) {
			return null;
		}else {
			return root.search(value);
		}
	}
	
	/**
	 * 删除节点
	 * @param value
	 */
	public void delete(int value) {
		if(root==null) {
			return;
		}else {
			//找到这个节点
			Node target = search(value);
			//如果没有这个节点
			if(target==null) {
				return;
			}
			//找到他的父节点
			Node parent = searchParent(value);
			//要删除的节点是叶子节点
			if(target.left==null&&target.right==null) {
				//要删除的节点是父节点的左子节点
				if(parent.left.value==value) {
					parent.left=null;
					//要删除的节点是父节点的右子节点
				}else {
					parent.right=null;
				}
			//要删除的节点有两个子节点的情况
			}else if(target.left!=null&&target.right!=null) {
				//删除右子树中值最小的节点,取获取到该节点的值
				int min = deleteMin(target.right);
				//替换目标节点中的值
				target.value=min;
			//要删除的节点有一个左子节点或右子节点
			}else {
				//有左子节点
				if(target.left!=null) {
					//要删除的节点是父节点的左子节点
					if(parent.left.value==value) {
						parent.left=target.left;
						//要删除的节点是父节点的右子节点
					}else {
						parent.right=target.left;
					}
				//有右子节点
				}else {
					//要删除的节点是父节点的左子节点
					if(parent.left.value==value) {
						parent.left=target.right;
						//要删除的节点是父节点的右子节点
					}else {
						parent.right=target.right;
					}
				}
			}
		}
	}
	
	/**
	 * 删除一颗树中最小的节点
	 * @param right
	 * @return
	 */
	private int deleteMin(Node node) {
		Node target = node;
		//递归向左找
		while(target.left!=null) {
			target=target.left;
		}
		//删除最小的这个节点
		delete(target.value);
		return target.value;
	}

	/**
	 * 搜索父节点
	 * @param value
	 * @return
	 */
	public Node searchParent(int value) {
		if(root==null) {
			return null;
		}else {
			return root.searchParent(value);
		}
	}
}

测试

package basic.demo12;

public class TestBinarySortTree {
	public static void main(String[] args) {
//		int[] arr = new int[] {8,9,6,7,5,4};
		int[] arr = new int[] {8,9,5,4,6,7};
		//创建一颗二叉排序树
		BinarySortTree bst = new BinarySortTree();
		//循环添加
		for(int i:arr) {
			bst.add(new Node(i));
		}
		//查看高度
		System.out.println(bst.root.height());
		//
		System.out.println(bst.root.value);
	}
}

计算机数据存储的方式

数据的存储方式

img

内存

​ 优点:使用电信号来保存信息的,不存在机器操作,所以访问速度非常快

​ 缺点:造价高,断电后数据丢失。一般作为CPU的高速缓存

img

spindle:主轴

surface:盘面

track:磁道

sector:扇区

gap: 间隔

img

arm:传动臂

read/write head:磁头

磁盘:

​ 优点:造价低,容量大,断电数据不丢失

​ 缺点:由于存储介质的特性,再加上机械运动耗费时间,所以磁盘的速度较慢。

磁盘的预读:

​ 由于磁盘的读写速度问题,要尽量减少磁盘I/O操作。所以磁盘往往不是严格按需读取,而是每次都会预读,即使只需要一个字节,磁盘也会从这个位置开始,顺序向后读取一定长度的数据放入内存。这样做的理论依据是计算机科学中著名的局部性原理:

​ 当一个数据被用到时,其附近的数据也通常会马上被使用。

*预读的长度一般为页(page)的整倍数。*

页:

​ 页是计算机管理存储器的逻辑块,硬件及操作系统往往将主存和磁盘存储区分割为连续的大小相等的块,每个存储块称为一页(在许多操作系统中,页得大小通常为4k),主存和磁盘以页为单位交换数据。

​ 文件系统及数据库系统的设计者利用了磁盘预读原理,将一个节点的大小设为等于一个页,这样每个节点只需要一次I/O就可以完全载入。

二叉树与B树:

wps4

将树的度M设置为1024,在600亿个元素中最多只需要4次I/O操作就可以读取到想要的元素。

2-3树和2-3-4树

是一种特殊的B树

B树中所有的叶节点都在同一层 有两个子节点的节点叫二节点 :二节点要么有两个子节点, 要么没有子节点 有三个子节点的节点三节点 :三节点要么有三个子节点,要么没有子节点

6、10、4、14、5、11、15、3、2、12、1、7、8、8、6

image-20200527163951260

image-20200527164034627

image-20200527164046056

image-20200527164054853

image-20200527164112724

image-20200527164120758

image-20200527164126941

B树

image-20200527164618863

哈希表

也叫散列表

散列函数

直接定址法

数字分析法

平方取中法

取余法

随机数法

image-20200527201048720

package basic.demo14;

/**
 * 顶点类
 * @author Richard
 */
public class Vertex {

	private String value;
	public boolean visited;

	public String getValue() {
		return value;
	}

	public void setValue(String value) {
		this.value = value;
	}

	public Vertex(String value) {
		super();
		this.value = value;
	}

	@Override
	public String toString() {
		return value;
	}

}

package basic.demo14;


import basic.b02.MyStack;

/**
 * 图
 * @author Richard
 *
 */
public class Graph {

	private Vertex[] vertex;
	private int currentSize;
	public int[][] adjMat;
	private MyStack stack = new MyStack();
	//当前遍历的下标
	private int currentIndex;
	
	public Graph(int size) {
		vertex=new Vertex[size];
		adjMat=new int[size][size];
	}
	
	/**
	 * 向图中加入一个顶点
	 * @param v
	 */
	public void addVertex(Vertex v) {
		vertex[currentSize++]=v;
	}
	
	public void addEdge(String v1,String v2) {
		//找出两个顶点的下标
		int index1=0;
		for(int i=0;i<vertex.length;i++) {
			if(vertex[i].getValue().equals(v1)) {
				index1=i;
				break;
			}
		}
		int index2=0;
		for(int i=0;i<vertex.length;i++) {
			if(vertex[i].getValue().equals(v2)) {
				index2=i;
				break;
			}
		}
		adjMat[index1][index2]=1;
		adjMat[index2][index1]=1;
	}
	
	/**
	 * 深度优先搜索算法遍历图
	 */
	public void dfs() {
		//把第0个顶点标记为已访问状态
		vertex[0].visited=true;
		//把第0个顶点的下标。
		stack.push(0);
		//打印顶点的值
		System.out.println(vertex[0].getValue());
		//遍历
		out:while(!stack.isEmpty()) {
			for(int i=currentIndex+1;i<vertex.length;i++) {
				//如果和下一个遍历的元素是通的
				if(adjMat[currentIndex][i]==1&&vertex[i].visited==false) {
					//把下一个元素压入栈中
					stack.push(i);
					vertex[i].visited=true;
					System.out.println(vertex[i].getValue());
					continue out;
				}
			}
			//弹出栈顶元素
			stack.pop();
			//修改当前位置为栈顶元素的位置
			if(!stack.isEmpty()) {
				currentIndex=stack.peek();
			}
		}
	}
	
}

package basic.demo14;

import java.util.Arrays;

public class TestGraph {

	public static void main(String[] args) {
		Vertex v1 = new Vertex("A");
		Vertex v2 = new Vertex("B");
		Vertex v3 = new Vertex("C");
		Vertex v4 = new Vertex("D");
		Vertex v5 = new Vertex("E");
		Graph g = new Graph(5);
		g.addVertex(v1);
		g.addVertex(v2);
		g.addVertex(v3);
		g.addVertex(v4);
		g.addVertex(v5);
		
		//增加边
		g.addEdge("A", "C");
		g.addEdge("B", "C");
		g.addEdge("A", "B");
		g.addEdge("B", "D");
		g.addEdge("B", "E");
		
		for(int[] a:g.adjMat) {
			System.out.println(Arrays.toString(a));
		}
		//深度优先遍历
		g.dfs();
	}
	
}

蓝桥杯准备

入门训练 圆的面积

#include<stdio.h>
#include<iostream>
int main (){
	int r;
	scanf("%d",&r);
	double pi = 3.14159265358979323,s;
	s=pi * r *r;
	printf("%.7f",s);
	return 0;
}

小数点的控制

%d	            按十进制整型数据的实际长度输出。

%ld	            输出长整型数据。

%lld	        输出长长整型数据。

%md	            m 为指定的输出字段的宽度。如果数据的位数小于 m,则左端补以空格,若大于 m,则按实际位数输出。

%u	            输出无符号整型(unsigned)。输出无符号整型时也可以用 %d,这时是将无符号转换成有符号数,然后输出。但编程的时候最好不要这么写,因为这样要进行一次转换,使 CPU 多做一次无用功。

%c	            用来输出一个字符。

%f	            用来输出实数,包括单精度和双精度,以小数形式输出。不指定字段宽度,由系统自动指定,整数部分全部输出,小数部分输出 6 位,超过 6 位的四舍五入。

%.mf	        输出实数时小数点后保留 m 位,注意 m 前面有个点。

%o	            以八进制整数形式输出,这个就用得很少了,了解一下就行了。

%s	            用来输出字符串。用 %s 输出字符串同前面直接输出字符串是一样的。但是此时要先定义字符数组或字符指针存储或指向字符串,这个稍后再讲。

%x(或 %X 或 %#x 或 %#X)	    以十六进制形式输出整数,这个很重要。

入门训练 序列求和

#include<stdio.h>
#include<iostream>
using namespace std;
int main (){
	long long int n;
	scanf("%d",&n);
	long long int count=0;
	count = n*(n+1)/2;
	printf("%lld",count);
	return 0;
	
}

注意:long long int的输出格式是%lld

  1. 短整型short:所占内存大小:2byte=16bit;

    所能表示范围:-32768~32767;(即-2^15~2^15-1)

  2. 整型int:所占内存大小:4byte=32bit;

    所能表示范围:-2147483648~2147483647;(即-2^31~2^31-1)

    unsigned: 所占内存大小:4byte=32bit;

    所能表示范围:0~4294967295;(即0~2^32-1)

  3. 长整型long:所占内存大小:4byte=32bit;

    所能表示范围:-2147483648~2147483647;(即-2^31~2^31-1)

    unsigned long: 所占内存大小:4byte=32bit;

    所能表示范围:0~4294967295;(即0~2^32-1)

注:上面所说的全部是有符号型的,short,int,long都默认为有符号型,其中long和int都占4个字节的空间大小,他们有什么区别呢?

C语言规定:无论什么平台都要保证long型占用字节数不小于int型, int型不小于short型。

  1. 字符型char:所占内存大小:1byte=8bit;

    所能表示范围:不确定!!!!;

    unsigned char:所占内存大小:1byte=8bit;

    所能表示范围:0~255;(0~2^8-1)

    singned char: 所占内存大小:1byte=8bit;

    所能表示范围:-128~127;(-2^7~2^7-1)

char的默认类型不确定有可能是unsigned,也有可能是signed,主要更具编译器而定,可以自己测试一下编译器的默认char的符号类型。

  1. 布尔类型bool:所占内存大小:1byte=8bit;

    所能表示的范围:只能取两个值false或者true;所以最小值就是:0, 最大值:1.

  2. 单精度float: 所占内存大小:4byte=32bit;

    所能表示的范围:(1.17549e-038)~(3.40282e+038);

注意:浮点数在内存中都是按科学计数法来存储的,浮点数的精度是由尾数的位数决定 的,大家记住即可不 必深究;

  1. 双精度double:所占内存大小:8byte=32bit;

    所能表示的范围:(2.22507e-308)~(1.79769e+308);

注:如何区分和使用这两个浮点类型呢,首先float和double的精度不同,float保留到小数点后面7位,而double保留到小数点后面16位,float能保证6位有效数字,而double能保证15位有效数字,如果在不追求精度的的情况下当然用 float比较好,节省内存,如果需要很高的精度的情况下,最好还是用double,平时我们定义浮点型变量一般都用double,毕竟精度高,一般精度的损失是不能忽略的。

  1. 字符串string:由于string在c++中属于类类型,不是基本数据类型,类不能计算其在内存中所占大小,非要用sizeof(string)来算的话,一般算出来的结果是 sizeof(string)=4byte, 如果string字符串内容很多,很明显就不是其真实大小,string类里面有计算其字节大小的函数如:size(),length()。

排序

演示网站

交换排序

冒泡排序

1.原理:比较两个相邻的元素,将值大的元素交换到右边

2.思路:依次比较相邻的两个数,将比较小的数放在前面,比较大的数放在后面。

    (1)第一次比较:首先比较第一和第二个数,将小数放在前面,将大数放在后面。

    (2)比较第2和第3个数,将小数 放在前面,大数放在后面。

    ……

    (3)如此继续,知道比较到最后的两个数,将小数放在前面,大数放在后面,重复步骤,直至全部排序完成

    (4)在上面一趟比较完成后,最后一个数一定是数组中最大的一个数,所以在比较第二趟的时候,最后一个数是不参加比较的。

    (5)在第二趟比较完成后,倒数第二个数也一定是数组中倒数第二大数,所以在第三趟的比较中,最后两个数是不参与比较的。

    (6)依次类推,每一趟比较次数减少依次

C++实现

//冒泡排序
/*
设计思路:两两比较,进行交换,将较大的放在后面, 
*/
void f2(){
	int n;
	int nums[201];
	scanf("%d",&n);
	for(int i =0;i<n;i++){
		scanf("%d",&nums[i]);	
	}
	//冒泡排序 
	for(int i=0;i<n;i++){
		//前一个和后一个比较,k要和k+1进行比较所以范围应该是n-1-i 
		for(int k =0;k<n-1-i;k++){
			//把大的放在后面 
			if(nums[k]>nums[k+1]){
				int num =nums[k];
				nums[k]=nums[k+1];
				nums[k+1]=num;
			}
		}
	}
	for(int i=0;i<n;i++){
		printf("%d ",nums[i]);
	}	
} 

java实现

public class BubbleSort {
	public static void main(String[] args) {
		int[] arr=new int[] {5,7,2,9,4,1,0,5,7};
		System.out.println(Arrays.toString(arr));
		bubbleSort(arr);
		System.out.println(Arrays.toString(arr));
	}
	
	//冒泡排序 每一轮选择一个最大的
	/**
	 * 5,7,2,9,4,1,0,5,7		共需要比较length-1轮
	 * 5,7,2,9,4,1,0,5,7	
	 * 5,2,7,9,4,1,0,5,7
	 * 5,2,7,4,1,0,5,7,9
	 */
	public static void bubbleSort(int[]  arr) {
		//控制共比较多少轮 length-1:最后一个不需要比较
		for(int i=0;i<arr.length-1;i++) {
			//控制比较的次数
			for(int j=0;j<arr.length-1-i;j++) {
				if(arr[j]>arr[j+1]) {
					int temp=arr[j];
					arr[j]=arr[j+1];
					arr[j+1]=temp;
				}
			}
		}
		
	}
}

快速排序

快速排序算法是一种基于交换的高效的排序算法,它采用了分治法的思想:

1、从数列中取出一个数作为基准数(枢轴,pivot)。

2、将数组进行划分(partition),将比基准数大的元素都移至枢轴右边,将小于等于基准数的元素都移至枢轴左边。

3、再对左右的子区间重复第二步的划分操作,直至每个子区间只有一个元素。

快排最重要的一步就是划分了。划分的过程用通俗的语言讲就是“挖坑”和“填坑”。

C++实现

//交换 
void swap (int& a, int& b);
////找基准数 划分 
int partition(int arr[], int left, int right) ;
//快速排序的递归算法 
void quickSort(int arr[], int left, int right);
//快速排序(分区排序)
void f8 (){
	int nums[201],n;
	scanf("%d",&n);
	for(int i =0;i<n;i++){
		scanf("%d",&nums[i]);		
	}
	quickSort(nums,0,n-1);//[0,n-1]的元素进行排序 
	for(int i=0;i<n;i++){
		printf("%d ",nums[i]);
	}	
}
//快速排序的递归算法 
void quickSort(int arr[], int left, int right){
     if (left <right){//序列长度小于或等于1不处理 
	   	 int j = partition(arr, left, right);//一趟划分 
	     quickSort(arr, left, j - 1);//递归左边子序列 
	     quickSort(arr, j + 1, right);//递归右边 子序列 
	 }
}
//自左向右一趟,每遇到比基准小的交换到左边,函数返回基准移动到的位置 ;
//找到一个元素左边的都比它小,右边的都比它大 
int partition(int arr[], int left, int right) { 
	//k:基准元素的位置 
	int i,k=left;
	int pivot = arr[left];//取出基准元素 (刨坑) 
	for(i=left+1;i<=right;i++){//一趟扫描整个序列进行划分 [left+1--right] 
		if(arr[i]<pivot){//检测到排序码小于基准的元素 
			k++;//(k指向的元素一定比基准元素小)  
			if(k!=i){
				swap(arr[i], arr[k]);//把小的元素交换到左边去 
			}
		}
	}
	arr[left]=arr[k];// 填坑 
	arr[k]=pivot;//将基准元素归位 
	return k;//返回基准元素的位置 
}
void swap (int& a, int& b){
    int c = a;
    a = b;
    b = c;
}

java实现

public class QuickSort {
	public static void main(String[] args) {
		int[] arr = new int[] {3,4,6,7,2,7,2,8,0,9,1};
		quickSort(arr,0,arr.length-1);
		System.out.println(Arrays.toString(arr));
	}
	public static void quickSort(int[] arr,int start,int end) {
		if(start<end) {
			//把数组中的第0个数字做为标准数
			int stard=arr[start];
			//记录需要排序的下标
			int low=start;
			int high=end;
			//循环找比标准数大的数和比标准数小的数
			while(low<high) {
				//右边的数字比标准数大
				while(low<high&&arr[high]>=stard) {
					high--;//右指针往左移
				}
				//右指针指向了一个数字比目标数小(该数字要放到左边),使用右边的数字替换左边的数
				arr[low]=arr[high];
				//如果左边的数字比标准数小
				while(low<high&&arr[low]<=stard) {
					low++;//左指针往右移
				}
				//左指针指向的数字大于目标数,该数字要放到右边
				arr[high]=arr[low];
			}
			//把标准数赋给低所在的位置的元素(高位也行,二者重合了)
			arr[low]=stard;
			//处理所有的小的数字
			quickSort(arr, start, low-1);
			//处理所有的大的数字
			quickSort(arr, low+1, end);
		}
	}
    public static void quickSort1(int[] arr, int start, int end) {
		if (start < end) {
			int i, k = start;
			int pivot = arr[start];
			for (i = start+1; i <= end; i++) {
				if (arr[i] < pivot) {
					k++;
					if (k != i) {
						int temp = arr[i];
						arr[i] = arr[k];
						arr[k] = temp;
					}
				}
			}
			arr[start] = arr[k];
			arr[k] = pivot;
			//处理所有的小的数字
			quickSort(arr, start, k - 1);
			//处理所有的大的数字
			quickSort(arr, k + 1, end);
		}
	}
}

改进快速排序

快速排序是一种 效率很高的排序算法,对于n较大的平均情况而言,快速排序是快速的,但当n很小时,这种排序方法往往比其它简单排序方法还要慢,研究表明序列长度M取值为5~25时使用直接插入排序要比快速排序至少快10%因此对快速排序算法进行改进的一个简单的方法就是:在递归调用过程中,当排序的子序列规模小于预先定义的M时,程序直接调用直接插入排序算法对子序列进行排序。

//快速排序的改进算法
//快速--直接插入排序
void quickSortInsert(int arr[],int len,int M) {
	
	if(len<=M){
		//调用直接插入排序
	}else{
		//快速排序 
	} 	
}

插入排序

直接插入排序

直接插入插排的基本思想是:当插入第i(i >= 1)时,前面的V[0],V[1],……,V[i-1]已经排好序。这时,用V[I]的排序码与V[i-1],V[i-2],…的排序码顺序进行比较,找到插入位置即将V[i]插入,原来位置上的元素向后顺移。

C++实现

//插入排序 
//把数据放在它应该在的地方,之后的数据依次后移 
void f1(){
	int n;
	int nums[201];
	scanf("%d",&n);
	for(int i =0;i<n;i++){
		scanf("%d",&nums[i]);	
	}
	//插入排序 ,第一个数据一定是有序的,因此是从1——n-1 
	for(int i=1;i<n;i++){
		//将第i个元素取出,放入应当放入的位置(从i-1到0) 
		int temp = nums[i]; 
		for(int k =i-1;k>=0;k--){
			//遇见大的数把这个数向后挪 ,并填坑 
			if(nums[k]>temp){
				nums[k+1]=nums[k];
				nums[k]=temp;
			}else{//遇见比这个数小的就停止 
				break;
			}
		}
	}
	for(int i=0;i<n;i++){
		printf("%d ",nums[i]);
	}	
}

java实现

package basic.b04sort;

import java.util.Arrays;

public class InsertSort {
	
	public static void main(String[] args) {
		int[] arr = new int[] {5,3,2,8,5,9,1,0};
		//insertSort(arr);
		insertSort1(arr);
		System.out.println(Arrays.toString(arr));
	}

	//插入排序
	public static void insertSort(int[] arr) {
		//遍历所有的数字:第一个元素一定是有序的
		for(int i=1;i<arr.length;i++) {
			//如果当前数字比前一个数字小
			if(arr[i]<arr[i-1]) {
				//把当前遍历数字存起来
				int temp=arr[i];
				int j;
				//遍历当前数字前面所有的数字
				for(j=i-1;j>=0&&temp<arr[j];j--) {
					//把前一个数字赋给后一个数字
					arr[j+1]=arr[j];
				}
				//把临时变量(外层for循环的当前元素)赋给不满足条件的后一个元素
				arr[j+1]=temp;
			}
		}
	}
	public static void insertSort1(int[] arr) {
		for (int i  =1;i<arr.length;i++){
			int temp = arr[i];//
			for (int k = i - 1; k >= 0; k--) {
				if (arr[k] > temp) {
					arr[k + 1] = arr[k];
					arr[k] = temp;
				} else {
					break;
				}
			}
		}
	}
	}

折半插入排序

二分法排序的思想,在插入第i个元素时,对前面的0~i-1元素进行折半,先跟他们中间的那个元素比,如果小,则对前半再进行折半,否则对后半进行折半,直到left>right,然后再把第i个元素前1位与目标位置之间的所有元素后移,再把第i个元素放在目标位置上

C++实现

//折半插入排序(二分排序)
void f6(){
	int nums[201],n;
	scanf("%d",&n);
	for(int i =0;i<n;i++){
		scanf("%d",&nums[i]);		
	}
	//第一个一定时有序的(从1——n-1) 
	for(int i =1;i<n;i++){//逐步扩大有序数组 
		int temp = nums[i];//取出待排序元素 
		int low =0,height=i-1,mid;//定义区间 
		while(low<=height){//利用折半查找寻找插入位置 
			mid = (low+height)/2;//取中点 
			if(temp<nums[mid]){//左区间 
				height = mid-1;
			}else{//否则右区间 
				low = mid+1;
			}
		}
		for(int j=i-1;j>=low;j--){//将i-1到low(height,mid)向后挪一格(i-1移到i) 
			nums[j+1]=nums[j];
		}
		nums[low]=temp;//插入 		
	} 
	for(int i=0;i<n;i++){
		printf("%d ",nums[i]);
	}	
} 


Java实现

public class BinaryInsertSort {
    public static void main(String[] args) {
        int[] arr=new int[] {5,7,2,9,4,1,0,5,7};
        System.out.println(Arrays.toString(arr));
        binaryInsertSort(arr);
        System.out.println(Arrays.toString(arr));
    }
    public static void binaryInsertSort(int[]arr) {
        for (int i = 1; i < arr.length; i++) {
            int temp = arr[i];
            int mid ,low = 0, height = i - 1;
            while (low <= height) {
                mid = (low + height) / 2;//取中点
                if (temp < arr[mid]) {
                    height = mid - 1;
                } else {
                    low = mid + 1;
                }
            }
            for (int j = i - 1; j >= low; j--) {
                arr[j+1] = arr[j];
            }
            arr[low] = temp;

        }
    }
}

希尔排序

C++实现

//希尔排序(缩小增量排序)
/*
区块内有序 ,每隔gap个元素去一个值,组成一组,组成gap组

将这gap组排好序,然后缩小间隔,在进行排序,直至间隔为1 
*/
void f7(){
	int nums[201],n;
	scanf("%d",&n);
	for(int i =0;i<n;i++){
		scanf("%d",&nums[i]);		
	}
//	int incregap = 3;//步进方式 
//	
//	unsigned gap = n/incregap+1;//步长初始化,注意如果当n<incregap时,gap为0,所以为了保证进入循环,gap至少为1!!! 

	//步长初始化一般为gap=n/incregap;缩小方式一般为gap=gap/incregap 
	int incregap = 2;//步进方式 
	unsigned gap = n/incregap;//步长初始化(每隔gap个元素选中一个,组成一组)
	int insertNum = 0;//
	while(gap){//while gap>=1 
		//分组,在每个子序列中进行插入排序(插入排序是将元素与之前的数据进行比较,
		//插入适当的位置,第一个元素一定是有序的, 需要排序是应该是gap--n-1)
        for (unsigned i = gap; i < n; i++){  
            insertNum = nums[i];//将当前的元素值先存起来方便后面插入
            unsigned j = i;//j是插入位置 
            while (j >= gap && insertNum < nums[j-gap]){//寻找插入位置 nums[j-gap](该元素比前一个元素小,该元素就要前移,前一个元素就要后移) 
                nums[j] = nums[j - gap];//前一个元素后移 
                j -= gap;//j要大于等于0,所以j>=gap, 
            }
            nums[j] = insertNum;//找到位置,插入该元素 
        }
        gap = gap/incregap;	//缩小步长 
	}
	for(int i=0;i<n;i++){
		printf("%d ",nums[i]);
	}		
} 

Java实现

/*希尔排序*/
public class ShellSort {

	public static void main(String[] args) {
		int[] arr = new int[] { 3, 5, 2, 7, 8, 1, 2, 0, 4, 7, 4, 3, 8 };
		System.out.println(Arrays.toString(arr));
		shellSort(arr);
		System.out.println(Arrays.toString(arr));
	}
	
	public static void shellSort(int[] arr) {
		int k = 1;
		// 遍历所有的步长
		for (int d = arr.length / 2; d > 0; d /= 2) {
			// 遍历所有有元素
			for (int i = d; i < arr.length; i++) {
				// 遍历本组中所有的元素
				for (int j = i - d; j >= 0; j -= d) {
					// 如果当前元素大于加上步长后的那个元素
					if (arr[j] > arr[j + d]) {
						int temp = arr[j];
						arr[j] = arr[j + d];
						arr[j + d] = temp;
					}
				}
			}
			System.out.println("第" + k + "次排序结果:" + Arrays.toString(arr));
			k++;
		}
	}

}

选择排序

简单选择排序

基本思想:假设排序表为 L[1….n] ,第i趟排序即从L[i,,,,n] 中选择关键字最小的元素与 L(i) 交换,每一趟排序可以确定一个元素的最终位置,这样经过 n-1 趟排序就可以使整个排序表有序。

C++实现

//选择排序 :每次从数组中选择一个最小的数的下标,然后与待排序的进行交换 
void f4(){
	int nums[201],n;
	scanf("%d",&n);
	for(int i =0;i<n;i++){
		scanf("%d",&nums[i]);	
	}
	//选择排序 ,最后一个数不需要考虑,因为到最后这个数字必然是最大的 
	for(int i=0;i<n-1;i++){
		int min = i;
		for(int k =i+1;k<n;k++){
			if(nums[k]<nums[min]){
				min = k;
			}
		}
		if(i!=min){
			int temp = nums[i];
			nums[i]=nums[min];
			nums[min]=temp;
		}
	}
	for(int i=0;i<n;i++){
		printf("%d ",nums[i]);
	}
}

Java实现

/*简单选择排序*/
public class SelectSort {

	public static void main(String[] args) {
		int[] arr = new int[] {3,4,5,7,1,2,0,3,6,8};
		selectSort(arr);
		System.out.println(Arrays.toString(arr));
	}
	
	//选择排序
	public static void selectSort(int[] arr) {
		//遍历所有的数
		for(int i=0;i<arr.length;i++) {
			int minIndex=i;
			//把当前遍历的数和后面所有的数依次进行比较,并记录下最小的数的下标
			for(int j=i+1;j<arr.length;j++) {
				//如果后面比较的数比记录的最小的数小。
				if(arr[minIndex]>arr[j]) {
					//记录下最小的那个数的下标
					minIndex=j;
				}
			}
			//如果最小的数和当前遍历数的下标不一致,说明下标为minIndex的数比当前遍历的数更小。
			if(i!=minIndex) {
				int temp=arr[i];
				arr[i]=arr[minIndex];
				arr[minIndex]=temp;
			}
		}
	}
}

堆排序

1、算法思想

堆排序(Heapsort)是指利用堆这种数据结构所设计的一种排序算法。堆积是一个近似完全二叉树的结构, 并同时满足堆积的性质:即子结点的键值或索引总是小于(或者大于)它的父节点。

2、实现原理 要实现从小到大的排序,就要建立大顶堆,即父节点比子节点都要大。

2.1、初始化数组,创建大顶堆。

大顶堆的创建从下往上比较,不能直接用无序数组从根节点比较,否则有的不符合大顶堆的定义。

2.2、交换根节点和倒数第一个数据,现在倒数第一个数据就是最大的。

2.3、重新建立大顶堆。

因为只有 array[0] 改变,其它都符合大顶堆的定义,所以可以根节点 array[0] 重新建立。

2.4、重复2.2、2.3的步骤,直到只剩根节点 array[0],即 i=1。

C++实现

//从大到小排序 
//void Down(int array[],int i,int n){
//	int child=2*i+1;
//	int key=array[i];
//	while (child<n){
//		if (array[child]>array[child+1] && child+1<n) {
//			child++;
//		}
//		if (key>array[child]){
//			swap(array, i, child);
//			i=child;
//		}
//		else{
//			break;
//		}
//		child=child*2+1;
//	}
//}
//从小到大排序
//生成大顶堆函数
//*****注意变量的取值范围; 
void Down(int array[],int i,int n){						//最后结果就是大顶堆 
	int parent=i;										//父节点下标
	int child=2*i+1;									//左子节点下标 ,指向子节点数值较大的那个 
	while (child<n){									//处理子节点的二叉树的大小 
		if (array[child]<array[child+1] && child+1<n) {	//判断子节点那个大,大的与父节点比较 
			child++;
		}
		if (array[parent]<array[child]){				//判断父节点是否小于子节点 
			swap(array[parent], array[child]);			//交换父节点和子节点 
			parent=child;								//子节点下标 赋给 父节点下标 (子节点变父节点,继续比较) 
		}

		child=child*2+1;								//比较子节点的子节点 
	}
}
//初始化大顶堆函数
void BuildHeap(int array[],int size)
{
   //构建大根堆从最后一个节点的父节点(size/2-1)开始向上(0)构建 [size/2-1,0] 
   //size/2-1:编号最大的分支节点
   //数组以0开始,最后一个元素为size-1,他的父节点为 (size-1+1)/2-1 
   for( int i=size/2-1;i>=0;i--){		//从最后一个元素的父节点一直到根节点 
      Down(array,i,size);	//轮流以i,i-1,i-2···0为根,将他们控制的字数调整为大根堆							 
	}
}
//排序函数
void heapSort(int array[],int size){
    BuildHeap(array,size);//初始化堆 ,建立大顶堆 
	//交换大顶堆的第一个元素(这个元素是最大的)和最后一个元素的位置,因此它是倒序的[size-1,0)第一个元素一定是有序的					
    for(int i=size-1;i>0;i--)
    {	
    	swap(array[0],array[i]);		//交换顶点和第 i 个数据 
    	//因为只有array[0]改变,其它都符合大顶堆的定义,所以可以从上往下重新建立 
    	Down(array,0,i);								//重新建立大顶堆 
	}
}
void f9(){
	
	int nums[201],n;
	scanf("%d",&n);
	for(int i =0;i<n;i++){
		scanf("%d",&nums[i]);		
	}
	heapSort(nums,n);//[0,n-1]的元素进行排序 
	for(int i=0;i<n;i++){
		printf("%d ",nums[i]);
	}	
	
}

Java实现

/*堆排序*/
public class HeapSort {
	
	public static void main(String[] args) {
		int[] arr = new int[] {9,6,8,7,0,1,10,4,2};
		heapSort(arr);
		System.out.println(Arrays.toString(arr));
	}
	
	public static void heapSort(int[] arr) {
		//开始位置是最后一个非叶子节点,即最后一个节点的父节点
		int start = (arr.length-1)/2;
		//调整为大顶堆
		for(int i=start;i>=0;i--) {
			maxHeap(arr, arr.length, i);
		}
		//先把数组中的第0个和堆中的最后一个数交换位置,再把前面的处理为大顶堆
		for(int i=arr.length-1;i>0;i--) {
			int temp = arr[0];
			arr[0]=arr[i];
			arr[i]=temp;
			maxHeap(arr, i, 0);
		}
	}
	
	public static void maxHeap(int[] arr,int size,int index) {
		//左子节点
		int leftNode = 2*index+1;
		//右子节点
		int rightNode = 2*index+2;
		int max = index;
		//和两个子节点分别对比,找出最大的节点
		if(leftNode<size&&arr[leftNode]>arr[max]) {
			max=leftNode;
		}
		if(rightNode<size&&arr[rightNode]>arr[max]) {
			max=rightNode;
		}
		//交换位置
		if(max!=index) {
			int temp=arr[index];
			arr[index]=arr[max];
			arr[max]=temp;
			//交换位置以后,可能会破坏之前排好的堆,所以,之前的排好的堆需要重新调整
			maxHeap(arr, size, max);
		}
	}
}

归并排序

二路归并排序

归并排序是一种概念上最为简单的排序算法,他与快速排序算法一样,归并排序算法也是基于分治法。归并排序将待排序的元素序列分为两个长度相等的子序列,为每个子序列排序,然后再将他们合并为一个序列。合并两个子序列的过程被称为二路归并(合并)

算法分析:

  • time-complexity: 总共需要进行log2n趟排序,每次排序执行n次基本操作,整个二路归并排序执行次数为nlog2n,时间复杂度为O(nlog2n)

    • space-complexity: 整个二路归并排序需要转存整个序列temp[len],因此空间复杂度为O(1) 算法稳定.

C++实现

//将两个非降序序列low--mid,mid+1--high合并为一个新的非降序序列
void Merge(int a[],int low,int mid,int high)
{
    int len = high-low+1;//长度 
    int *temp = new int[len];//新结果数组 
    int i = low,j = mid+1;    //i,j分别为两个子序列的游标
    int k = 0;     //为新合并序列的游标
    while(i<=mid && j<=high){
        if(a[i]<=a[j]){
            temp[k] = a[i];
            k++;
            i++;
        }else{
            temp[k] = a[j];
            k++;
            j++;
        }
    }
    while(i<=mid){    //若第一个子序列有剩余,则直接接到尾部
        temp[k] = a[i];
        k++;
        i++;
    }
    while(j<=high){    //若第二个子序列有剩余,则直接接到尾部
        temp[k] = a[j];
        k++;
        j++;
    }
    //copy到a[]
    for(k=0;k<len;k++){
		a[low+k] = temp[k];
	} 
	delete []temp;
}

//low high为待排序列左右边界
void MergeSort(int a[],int low,int high)
{
    if(low<high){
        int mid = (low+high)/2;//从中间划分为两个子序列 
        MergeSort(a,low,mid);//对左边的子序列左递归归并排序 
        MergeSort(a,mid+1,high);//对右边的子序列左递归归并排序
        Merge(a,low,mid,high); //合并
    }
}
void f10(){
	int nums[201],n;
	scanf("%d",&n);
	for(int i =0;i<n;i++){
		scanf("%d",&nums[i]);		
	}
	MergeSort(nums,0,n-1);//[0,n-1]的元素进行排序 
	for(int i=0;i<n;i++){
		printf("%d ",nums[i]);
	}	
} 

Java实现

/*归并排序*/
public class MergeSort {

	public static void main(String[] args) {
		int[] arr = new int[] {1,3,5,2,4,6,8,10};
		System.out.println(Arrays.toString(arr));
		mergeSort(arr, 0, arr.length-1);
		System.out.println(Arrays.toString(arr));
	}
	
	//归并排序
	public static void mergeSort(int[] arr,int low,int high) {
		int middle=(high+low)/2;
		if(low<high) {
			//处理左边
			mergeSort(arr, low, middle);
			//处理右边
			mergeSort(arr, middle+1, high);
			//归并
			merge(arr,low,middle,high);
		}
	}
	
	public static void merge(int[] arr,int low,int middle, int high) {
		//用于存储归并后的临时数组
		int[] temp = new int[high-low+1];
		//记录第一个数组中需要遍历的下标
		int i=low;
		//记录第二个数组中需要遍历的下标
		int j=middle+1;
		//用于记录在临时数组中存放的下标
		int index=0;
		//遍历两个数组取出小的数字,放入临时数组中
		while(i<=middle&&j<=high) {
			//第一个数组的数据更小
			if(arr[i]<=arr[j]) {
				//把小的数据放入临时数组中
				temp[index]=arr[i];
				//让下标向后移一位;
				i++;
			}else {
				temp[index]=arr[j];
				j++;
			}
			index++;
		}
		//处理多余的数据
		while(j<=high) {
			temp[index]=arr[j];
			j++;
			index++;
		}
		while(i<=middle) {
			temp[index]=arr[i];
			i++;
			index++;
		}
		//把临时数组中的数据重新存入原数组
		for(int k=0;k<temp.length;k++) {
			arr[k+low]=temp[k];
		}
	}

}

基数排序

MSD

C++实现

/**********************************MSD基数排序********************************/
/**
比如说都是三位数(不足补零78-->078)先按照百位进行排序再按十位进行排序,最后按照个位进行排序
0-9视为一个个桶 
*/

#define d 3 //排序码位数(59-->059,)
#define radix 10 //基数(桶数);十进制整数 0-9 
//从整数位x中提取第k位数字,最高位算1,次高位算2···最低位算k 
int getDigit(int x,int k){
    if(k<1||k>d){//整数位不能超过d (排数数字不能大于d位) 
    	return -1;
	}
    for(int i =1;i<=d-k;i++){
    	x = x/10;
	} 
    return x%10;//提取x的第k位数字 
}
/* MSD桶排序算法是从高位到低位对序列进行分配,实现排序。k:第几位,n是待排序的元素的个书,
因为是递归排序,left和right是待排序元素子序列的首尾位置 
count[]辅助数组,用count[k]记录当处理第i各元素时各个元素的第i位取值为k的有多少个。k是属于基数radix的范围
0 1 2 3 4 5 6 7 8 9 i 
1 1 0 2 4 1 3 0 1 0 k
i位的数字有k个
auxArray辅助数组用来存放按桶分配的结果,根据count[]预先算定各桶元素的位置(在posit中),
每一趟向各桶分配结束后,元素都会被复制回原表中
*/
void radixSortMSD(int A[],int left, int right, int k){
    if(left>=right||k>d){
    	return ;
	}
	int i,j,v,p1,p2,count[radix],posit[radix];
	int *auxArray=(int*)malloc((right-left+1)*sizeof(int));//暂存数组,分配木桶 
	for(j=0;j<radix;j++){//数组初始化 
		count[j]=0;
	}
	for(i=left;i<=right;i++){
		v = getDigit(A[i],k);
		count[v]++;//统计各桶元素个数		
	}
	//posit中定义的是待排序元素排序存放在 auxArray中的位置 
	posit[0]=0;//第一个0的位置一定是0,第一个1的位置应该是0的数量(count[0]加上第一个0的位置(posit[0]),第一个2的位置应该是1的数量(count[1])加上第一个1的位置 (posit[1])
	/*	安排各桶元素位置 ,元素按位值分配到各桶 末位置 
		知道了每个元素第i位的数字的个数,就可以分配该元素的位置 (count[0]=1,count[1]=3;count[2]=2)
		元素存放的位置应该是0      1 2 3   	4 5 6 
						  0        1        2
		posit中的值指向了该位数字为1 的数字应当存放位置的起始位置,第一个1存放在 auxArray[1],第二个1存放在auxArray[2],第二个2应当存放在auxArray[5] 
	*/
	//定义每个数字存放的起始位置 
	for(j=1;j<radix;j++){
		posit[j]=count[j-1]+posit[j-1];
	}
	for(i=left;i<=right;i++){
		v = getDigit(A[i],k);//取元素A[i]第k位的值 
		auxArray[posit[v]++]=A[i];//按预先计算位置存放 ,posit[v]++(先使用后加一)并且将位置后移 
		
	}
	for(i=left,j=0;i<=right;i++,j++){
		
		A[i]=auxArray[j];	//从辅助数组写入原数组	
	}
	free(auxArray);
	p1=left;
	for(j=0;j<radix;j++){//按桶递归对k-1位处理 
		p2=p1+count[j]-1;//取子桶的首末位置 
		radixSortMSD(A,p1,p2,k+1);//对子桶内元素进行排序 
		p1=p2+1;// 	
	}
}
void f11(){
	int nums[201],n;
	scanf("%d",&n);
	for(int i =0;i<n;i++){
		scanf("%d",&nums[i]);		
	}
	//有局限性第三个参数指明了能排序的数字的最大值的位数-1,2:从百位进行排序,1:从十位进行排序,(不会排序比它大的数) 
	radixSortMSD(nums, 0, n-1,2);	
	for(int i=0;i<n;i++){
		printf("%d ",nums[i]);
	}	
} 

LSD

C++实现

待实现

/*
*求数据的最大位数,决定排序次数
*/
int maxbit(int data[], int n) 
{
    int d = 1; //保存最大的位数
    int p = 10;
    for(int i = 0; i < n; ++i)
    {
        while(data[i] >= p)
        {
            p *= 10;
            ++d;
        }
    }
    return d;
}
void radixsort(int data[], int n) //基数排序
{
    int d = maxbit(data, n);
    int tmp[n];
    int count[10]; //计数器
    int i, j, k;
    int radix = 1;
    for(i = 1; i <= d; i++) //进行d次排序
    {
        for(j = 0; j < 10; j++)
            count[j] = 0; //每次分配前清空计数器
        for(j = 0; j < n; j++)
        {
            k = (data[j] / radix) % 10; //统计每个桶中的记录数
            count[k]++;
        }
        for(j = 1; j < 10; j++)
            count[j] = count[j - 1] + count[j]; //将tmp中的位置依次分配给每个桶
        for(j = n - 1; j >= 0; j--) //将所有桶中记录依次收集到tmp中
        {
            k = (data[j] / radix) % 10;
            tmp[count[k] - 1] = data[j];
            count[k]--;
        }
        for(j = 0; j < n; j++) //将临时数组的内容复制到data中
            data[j] = tmp[j];
        radix = radix * 10;
    }

基数排序Java实现

public class RadixSort {

	public static void main(String[] args) {
		int[] arr = new int[] {23,6,189,45,9,287,56,1,798,34,65,652,5};
		radixSort(arr);
		System.out.println(Arrays.toString(arr));
	}
	
	public static void  radixSort(int[] arr) {
		//存最数组中最大的数字
		int max=Integer.MIN_VALUE;
		for(int i=0;i<arr.length;i++) {
			if(arr[i]>max) {
				max=arr[i];
			}
		}
		//计算最大数字是几位数
		int maxLength = (max+"").length();
		//用于临时存储数据的数组
		int[][] temp = new int[10][arr.length];
		//用于记录在temp中相应的数组中存放的数字的数量
		int[] counts = new int[10];
		//根据最大长度的数决定比较的次数
		for(int i=0,n=1;i<maxLength;i++,n*=10) {
			//把每一个数字分别计算余数
			for(int j=0;j<arr.length;j++) {
				//计算余数
				int ys = arr[j]/n%10;
				//把当前遍历的数据放入指定的数组中
				temp[ys][counts[ys]] = arr[j];
				//记录数量
				counts[ys]++;
			}
			//记录取的元素需要放的位置
			int index=0;
			//把数字取出来
			for(int k=0;k<counts.length;k++) {
				//记录数量的数组中当前余数记录的数量不为0
				if(counts[k]!=0) {
					//循环取出元素
					for(int l=0;l<counts[k];l++) {
						//取出元素
						arr[index] = temp[k][l];
						//记录下一个位置
						index++;
					}
					//把数量置为0
					counts[k]=0;
				}
			}
		}
	}
	
}

队列实现

public class RadixQueueSort {

   public static void main(String[] args) {
      int[] arr = new int[] {23,6,189,45,9,287,56,1,798,34,65,652,5};
      radixSort(arr);
      System.out.println(Arrays.toString(arr));
   }
   
   public static void  radixSort(int[] arr) {
      //存最数组中最大的数字
      int max=Integer.MIN_VALUE;
      for(int i=0;i<arr.length;i++) {
         if(arr[i]>max) {
            max=arr[i];
         }
      }
      //计算最大数字是几位数
      int maxLength = (max+"").length();
      //用于临时存储数据的队列的数组
      MyQueue[] temp = new MyQueue[10];
      //为队列数组赋值
      for(int i=0;i<temp.length;i++) {
         temp[i]=new MyQueue();
      }
      //根据最大长度的数决定比较的次数
      for(int i=0,n=1;i<maxLength;i++,n*=10) {
         //把每一个数字分别计算余数
         for(int j=0;j<arr.length;j++) {
            //计算余数
            int ys = arr[j]/n%10;
            //把当前遍历的数据放入指定的队列中
            temp[ys].add(arr[j]);
         }
         //记录取的元素需要放的位置
         int index=0;
         //把所有队列中的数字取出来
         for(int k=0;k<temp.length;k++) {
            //循环取出元素
            while(!temp[k].isEmpty()) {
               //取出元素
               arr[index] = temp[k].poll();
               //记录下一个位置
               index++;
            }
         }
      }
   }  
}
/*冒泡排序*/
static int[] bubbleSort(int[] nums)
{
    //每次选出一个最大的放在最后面   ---   两两比较---每一轮都从索引0开始循环比较
    //或者每次选出一个最小的放在最前面 
    for(int i = 0; i < nums.Length-1; i++)
    {
        for(int j = 0; j < nums.Length - i - 1;j++)
        {
            if (nums[j] > nums[j + 1])
            {
                int temp = nums[j];
                nums[j] = nums[j+1];
                nums[j + 1] = temp;
            }
        }
    }
    return nums;
}
/*快速排序
             K位置左边的数字都要比k小
             K位置右边的数字都要比K大
             k是基准--一般取待排序数组的第一个元素;
             1.判断递归出口,if begin<end才执行该操作
             2.取出基准元素pivot;
             3.找到该基准元素的位置  K  K指向的元素是最后一个小于基准元素的元素(最后需要和基准元素beigin交换值),
                只有当一个元素小于基准元素的时候K才会自增
                对待排序的数组进行循环遍历,将所有小于基准的元素放到K++的位置,
                K指向的元素的位置就是基准元素pivot的位置,最后在交换二者的值
             4.递归左边的数组
             5.递归右边的数组
                 */
static void quickSort(int[] nums,int begin,int end)
{
    if (begin < end)
    {
        int pivot = nums[begin];//作为基准
        int i, k = begin;
        for (i = begin+1; i <= end; i++)
        {
            if (nums[i] < pivot)
            {
                k++;//K就指向了第一个大于目标元素的值,而且又遇到了一个小于目标元素的值,因此需要交换i和k位置的元素
                if (k != i)
                {
                    int temp = nums[i];
                    nums[i] = nums[k];
                    nums[k] = temp;
                }
            }
        }
        nums[begin] = nums[k];
        nums[k] = pivot;
        //处理所有的小的数字
        quickSort(nums, begin, k - 1);
        quickSort(nums, k + 1, end);
    }
}

/**
             插入排序
                假设K元素前面的元素都已经排好序,只需将k元素插入到前面的元素数组的适当的位置,比它大的后移
                1.对数组进行遍历,从第二个元素开始,因为第一个元素永远是有序的
                2.如果当前元素比前一个元素小,说明当前元素需要前移。如果大的话就不需要移动,现在的位置就应该是它所在的为hi在
                3.取出当前元素temp,对i之前的元素进行遍历,循环条件:1.数组遍历完毕;2.有一个元素比目标元素小。则结束循环
                4.如果该元素比目标元素temp大,说明该元素需要后移,
                5.将temp放到它应该在的位置j+1

                 */
static void insertSort(int[] nums)
{
    for(int i = 1; i < nums.Length; i++)
    {
        if (nums[i] < nums[i - 1])
        {
            int temp = nums[i];
            int j;
            for (j = i - 1; j >= 0 && nums[j] > temp; j--)
            {
                nums[j + 1] = nums[j];
            }
            nums[j + 1] = temp;
        }


    }
}
/*折半插入排序

                 插入排序是依次与前面的所有元素进行比较,折半插入排序是与中间的元素进行比较来逐步缩小范围
                 1.缩小范围
                 2.元素后移
                 3.元素归位

                 */
static void binaryInsertSort(int[] nums)
{
    for (int i = 1; i < nums.Length; i++) {
        if (nums[i] < nums[i - 1])
        {
            int temp = nums[i];
            int mid, low = 0, height = i - 1;//最后low和height指向temp应该在的位置
            while (low <= height)
            {
                mid = (low + height) / 2;
                if (temp < nums[mid])
                {
                    height = mid - 1;
                }
                else
                {
                    low = mid + 1;
                }
            }
            //将元素后移 范围是i-1到low,low的位置指向了temp应该在的位置
            for(int j = i - 1; j >= low; j--)
            {
                nums[j + 1] = nums[j];//元素后移
            }
            nums[low] = temp;//元素归位
        }
    }
}
/*希尔排序(缩小增量排序)
             * 思想是区块内有序,每隔gap个元素取一个值,使得这几个元素有序,然后缩小间隔再进行排序直至间隔为1
             * 
             * 没一组的元素是从后向前找的,与插入排序的思想类似,区块内的元素其实使用的就是插入排序来进行排序的
             * **/
static void shellSort(int[] nums)
{
    //遍历所有的步长  值从数组长度的一半,然后逐渐缩减一半
    for(int d = nums.Length / 2; d > 0; d /= 2)
    {
        //遍历所有的元素(从步长的位置的元素进行遍历)
        for(int i = d; i < nums.Length; i++)
        {
            //遍历本组元素 从后向前遍历本组元素(与插入排序类似)
            for(int j = i - d; j >= 0; j-= d)
            {
                //如果当前元素大于加上步长后的元素就交换位置
                if (nums[j] > nums[j + d])
                {
                    int temp = nums[j];
                    nums[j] = nums[j + d];
                    nums[j + d] = temp;
                }
            }
        }
    }
}
/**简单的选择排序
             每次选出一个最小或最大的元素,放在它应该在的位置

                 */
static void selectSort(int[] nums) {

    for(int i = 0; i < nums.Length; i++)
    {
        int min = i;
        for(int j = i + 1; j < nums.Length; j++)
        {
            if (nums[j] < nums[min])
            {
                min = j;
            }
        }
        if (min != i)
        {
            int temp = nums[i];
            nums[i] = nums[min];
            nums[min] = temp;
        }
    }
}
/**堆排序
                 堆分为大根堆和小根堆。即子节点的值总是小于或大于父节点的值
                 */
static void heapSort(int[] nums) {
    //最后一个非叶子结点,即最后一个结点的父节点
    int start = (nums.Length - 1) / 2;
    //调整为大根堆
    for(int i= start; i >= 0; i--)//自底向上的调整大根堆,应该从最后一个叶子结点的父节点开始,逐层向上进行构建大根堆
    {
        maxHeap(nums, nums.Length, i);//调整的堆为以结点i为根的堆
    }
    //先把数组中的第0个和数组中的最后一个数交换位置,再把前面的处理为大顶堆,因为数组中的第一个元素就是大根堆的堆,是这些数字中最大的数字,因此需要把他放在最后
    for (int i = nums.Length - 1; i > 0; i--)
    {
        int temp = nums[0];//取出大根堆的根节点  也就是最大的元素   与最后一个元素进行交换 在重新调整大根堆
        nums[0] = nums[i];
        nums[i] = temp;
        maxHeap(nums, i, 0);
    }
}
/**
             nums:堆中的数据
             size:待排序的元素的个数(因为是逐个逐个取出大根堆的堆顶的元素,所以待排序的元素的个数是逐步减少的)
             index:堆的根节点所在的位置
                 */
static void maxHeap(int[] nums,int size,int index)
{
    //左子节点
    int leftNode = 2 * index + 1;
    //右子节点
    int rightNode = 2 * index + 2;
    int max = index;
    if (leftNode < size && nums[leftNode] > nums[max])
    {
        max = leftNode;
    }
    if (rightNode < size && nums[rightNode] > nums[max])
    {
        max = rightNode;
    }
    //交换位置
    if (max != index)
    {
        int temp = nums[index];
        nums[index] = nums[max];
        nums[max] = temp;
        //交换位置后,可能会破坏之前排好序的堆,所以,之前排好序的堆需要重新调整
        maxHeap(nums, size, max);
    }

}
/**
             归并排序:
                二路归并排序:与快速排序算法一样,归并排序算法也是基于分治法的
                归并排序将待排序的元素序列分为两个长度相等的子序列,为每个子序列排序,然后再将他们合并为一个序列。
                合并两个子序列的过程被称为二路归并(合并)
                 */
static void mergeSort(int[] nums,int low,int height)
{

    int middle = (low + height) / 2;
    if (low < height)
    {
        mergeSort(nums, low, middle);
        mergeSort(nums, middle + 1, height);
        merge(nums, low, middle, height);
    }
}
/**用于归并算法的合并工作
             将两个已经排好序的数组合并为一个新的有序数组
            1.定义一个临时存储元素的数组
            2.定义三个下标,分别指向这三个数组的起始位置
            3.遍历两个数组,将较小的元素插入到temp数组中
            4.分别处理两个数组中剩下的元素。
            5.将temp数组中的元素还原到原数组中。
                 */
static void merge(int[] nums, int low, int middle, int height)
{
    //用于临时存储归并后的元素
    int[] temp = new int[height - low + 1];
    int i = low;
    int j = middle + 1;
    int index = 0;
    while (i <= middle && j <= height)
    {
        if (nums[i] <= nums[j])
        {
            temp[index++] = nums[i++];
        }
        else
        {
            temp[index++] = nums[j++];
        }
    }
    while (i <= middle)
    {
        temp[index++] = nums[i++];
    }
    while (j <= height)
    {
        temp[index++] = nums[j++];
    }
    for(int k = 0; k < nums.Length; k++)
    {
        nums[k] = temp[k];
    }
}

背包九讲

dd大牛的背包九讲

P01: 01背包问题

题目 有N件物品和一个容量为V的背包。第i件物品的费用是c[i],价值是w[i]。求解将哪些物品装入背包可使这些物品的费用总和不超过背包容量, 且价值总和最大。

基本思路

这是最基础的背包问题,特点是:每种物品仅有一件,可以选择放或不放。

用子问题定义状态:即f[i][v]表示前i件物品恰放入一个容量为v的背包可以获得的最大价值,(恰字表示:前i件物品,总体积是v的情况下的总价值)。则其状态转移方程便是:

 f[i][v]=max{f[i-1][v],f[i-1][v-c[i]]+w[i]}
 result = max{f[n][0...V]}

对于第i将物品我们只有两种选择

  1. 不选:那么最大的价值就是f[i-1][v](第i件物品不选的话,最终结果选择的物品就是前i-1件,前i-1件物品放入容量为v的背包中)
  2. 选:f[i-1][v-c[i]]+w[i]:w[i]为第i件物品的价值,f[i-1][v-c[i]]为前i-1件物品放入剩下的容量为v-c[i]的背包中的最优解

这个方程非常重要,基本上所有跟背包相关的问题的方程都是由它衍生出来的。所以有必要将它详细解释一下:“将前i件物品放入容量为v的背包中”这个子问题,若只考虑第i件物品的策略(放或不放),那么就可以转化为一个只牵扯前i-1件物品的问题。如果不放第i件物品,那么问题就转化为“前i-1件物品放入容量为v的背包中”;如果放第i件物品,那么问题就转化为“前i-1件物品放入剩下的容量为v-c[i]的背包中”,此时能获得的最大价值就是f [i-1][v-c[i]]再加上通过放入第i件物品获得的价值w[i]。

注意f[i][v]有意义当且仅当存在一个前i件物品的子集,其费用总和为v。所以按照这个方程递推完毕后,最终的答案并不一定是f[N] [V],而是f[N][0..V]的最大值。如果将状态的定义中的“恰”字去掉,在转移方程中就要再加入一项f[i][v-1],这样就可以保证f[N] [V]就是最后的答案。至于为什么这样就可以,由你自己来体会了。

优化空间复杂度 以上方法的时间和空间复杂度均为O(N*V),其中时间复杂度基本已经不能再优化了,但空间复杂度却可以优化到O(V)。f[i]只与f[i-1]`的值有关,没必要把整个一层的值记录下来,

先考虑上面讲的基本思路如何实现,肯定是有一个主循环i=1..N,每次算出来二维数组f[i][0..V]的所有值。那么,如果只用一个数组f [0..V],能不能保证第i次循环结束后f[v]中表示的就是我们定义的状态f[i][v]呢?f[i][v]是由f[i-1][v]f[i-1] [v-c[i]]两个子问题递推而来,能否保证在推f[i][v]时(也即在第i次主循环中推f[v]时)能够得到f[i-1][v]f[i-1][v -c[i]]的值呢?事实上,这要求在每次主循环中我们以v=V..0的顺序推f[v],这样才能保证推f[v]f[v-c[i]]保存的是状态f[i -1][v-c[i]]的值。伪代码如下:

f[v]表示体积小位等于v的情况下价值最大是多少

for i=1..N
	for v=V..0
		f[v]=max{f[v],f[v-c[i]]+w[i]};

其中的f[v]=max{f[v],f[v-c[i]]+w[i]};一句恰就相当于我们的转移方程f[i][v]=max{f[i-1][v],f[i- 1][v-c[i]]},因为现在的~就相当于原来的f[i-1][v-c[i]]。如果将v的循环顺序从上面的逆序改成顺序的话,那么则成了f[i][v]f[i][v-c[i]]推知,与本题意不符,但它却是另一个重要的背包问题P02最简捷的解决方案,故学习只用一维数组解01背包问题是十分必要的。

总结 01背包问题是最基本的背包问题,它包含了背包问题中设计状态、方程的最基本思想,另外,别的类型的背包问题往往也可以转换成01背包问题求解。故一定要仔细体会上面基本思路的得出方法,状态转移方程的意义,以及最后怎样优化的空间复杂度。

  • 0…V

    考虑二维的状态f[i][v]=max{f[i-1][v],f[i-1][v-c[i]]+w[i]}压缩为一维的状态,要进行等价变换,在计算 f[v]的时候 要用到f[v-c[i]]的状态,我们在计算第v个物品的时候要保证f[v-c[i]] 的值没有被计算过,也就是要用到二维中的f[i-1][v-c[i]]+w[i]的值,但压缩之后实际上用的是f[i][v-c[i]]+w[i]的状态。v在从小到大枚举的时候,在计算f[v]的时候f[v-c[i]]的值已经被计算过了,用的是二维中的f[i]而不是f[i-1]。在计算f[v]的时候如何保证f[v-c[i]]的值用到的是二维中的f[i-1]的值呢?就是要从大到小进行枚举

  • V…0

    v在从大到小枚举的时候,在计算f[v]的时候用到的f[v-c[i]]的时候,因为v-c[i]一定是小于v的,所以f[v-c[i]]值一定是没有被计算过的,用的是二维中的的f[i-1][v-c[i]]+w[i]的值。 最终的结果就是f[m],为什么不需要进行枚举呢?这个和f[N]数组的初始化有关

    f[0...N]=0;
    假设最终的总体积是k,k<m的话
    f[k]=max_w; 最大价值
    f[k]是从f[0]一步一步转移过来的
    f[0]=0 ==>f[v[0]] = w[0]... 选第一个物品
        
    因为k是最优解,f[m]是可以从f[m-k]的状态转移过来的
    f[m-k](初始值为0) =0 ==> f[m-k +v[m]] =w[0] ...选法和上面一样
    多了一个偏移量,f[m]的解包含了f[k]的解,所以f[m]一定是大于等于f[k]的结构的,而f[k]又是最大值,所以f[m]y
        
        
        
    要获取体积恰好为m的情况下最大的价值是多少?
    初始化时f[0]=0;f[i]=-INF; 确保所有的状态都是从f[0]中转移过来,因为从其他状态转移过来一定是负无穷
        
    

真题

有 N 件物品和一个容量是 W 的背包。每件物品只能使用一次。

第 i 件物品的体积是 vi,价值是wi。

求解将哪些物品装入背包,可使这些物品的总体积不超过背包容量,且总价值最大。 输出最大价值。

输入格式

第一行两个整数,N,W,用空格隔开,分别表示物品数量和背包容积。

接下来有 N 行,每行两个整数 vi,wi,用空格隔开,分别表示第 i件物品的体积和价值。

输出格式

输出一个整数,表示最大价值。

题解

二维数组版

/*
https://www.acwing.com/problem/content/2/
**/
#include<iostream>
#include<cstring>
#include<algorithm>

using namespace std;

const int N=1010;
int m,n;
int f[N][N];
int main(){
    cin>>n>>m;
    for(int i=1;i<=n;i++){
        int v,w;
        cin>>v>>w;
        for(int j=0;j<=m;j++){
            f[i][j]=f[i-1][j];
            if(j>=v){
                f[i][j]=max(f[i][j],f[i-1][j-v]+w);
            }
        }
    }
    int res = 0;
    for(int i=0;i<=m;i++){
        res= max(res,f[n][i]); 
    }
    cout<<res<<endl;
    return 0;
}

一维数组压缩版

/*
https://www.acwing.com/problem/content/2/
**/
#include<iostream>
#include<cstring>
#include<algorithm>
using namespace std;
const int N=1010;
int m,n;
int f[N];
int v[N],w[N];
int main(){
    cin>>n>>m;
    for(int i =1;i<=n;i++){
        cin>>v[i]>>w[i];
    }
    for(int i=1;i<=n;i++){
        for(int j=m;j>=v[i];j--){
            f[j]=max(f[j],f[j-v[i]]+w[i]);
        }
    }
    cout<<f[m]<<endl;
    return 0;
}

一维数组化简版

/*
https://www.acwing.com/problem/content/2/
**/
#include<iostream>
#include<cstring>
#include<algorithm>
using namespace std;
const int N=1010;
int n,m;//n表示物品数量  m表示背包体积
int f[N];//f[i] 表示背包容量为i的最优解
int main(){
    cin>>n>>m;
    for(int i=0;i<n;i++){
        int v,w;
        cin>>v>>w;
        for(int j=m;j>=v;j--){//背包容量从大到小进行枚举   v....m
            f[j]=max(f[j],f[j-v]+w);
        }
    }
    cout<<f[m]<<endl;
    return 0;
}
/**
初始值f[0....n]=0

*/

从大到小进行枚举的意义就是避免重复m,这是因为要保证第i次循环中的状态f[i][v]是由状态f[i-1][v-c[i]]递推而来。

换句话说,这正是为了保证每件物品只选一次,保证在考虑“选入第i件物品”这件策略时,依据的是一个绝无已经选入第i件物品的子结果f[i-1][v-c[i]]

P02: 完全背包问题

题目 有N种物品和一个容量为V的背包,每种物品都有无限件可用。第i种物品的费用是c[i],价值是w[i]。求解将哪些物品装入背包可使这些物品的费用总和不超过背包容量,且价值总和最大。

基本思路 这个问题非常类似于01背包问题,所不同的是每种物品有无限件。也就是从每种物品的角度考虑,与它相关的策略已并非取或不取两种,而是有取0件、取1件、取2件……等很多种。如果仍然按照解01背包时的思路,令f[i][v]表示前 i 种物品恰放入一个容量为v的背包的最大权值。仍然可以按照每种物品不同的策略写出状态转移方程,像这样:f[i][v]=max{f[i-1][v-k*c[i]]+k*w[i]|0<=k*c[i]<= v}。这跟01背包问题一样有O(N*V)个状态需要求解,但求解每个状态的时间则不是常数了,求解状态f[i][v]的时间是O(v/c[i]),总的复杂度是超过O(VN)的。

将01背包问题的基本思路加以改进,得到了这样一个清晰的方法。这说明01背包问题的方程的确是很重要,可以推及其它类型的背包问题。但我们还是试图改进这个复杂度。

一个简单有效的优化

完全背包问题有一个很简单有效的优化,是这样的:若两件物品i、j满足c[i]<=c[j]且w[i]>=w[j],则将物品j去掉,不用考虑。这个优化的正确性显然:任何情况下都可将价值小费用高得j换成物美价廉的i,得到至少不会更差的方案。对于随机生成的数据,这个方法往往会大大减少物品的件数,从而加快速度。然而这个并不能改善最坏情况的复杂度,因为有可能特别设计的数据可以一件物品也去不掉。

转化为01背包问题求解 既然01背包问题是最基本的背包问题,那么我们可以考虑把完全背包问题转化为01背包问题来解。最简单的想法是,考虑到第i种物品最多选V/c [i]件,于是可以把第i种物品转化为V/c[i]件费用及价值均不变的物品,然后求解这个01背包问题。这样完全没有改进基本思路的时间复杂度,但这毕竟给了我们将完全背包问题转化为01背包问题的思路:将一种物品拆成多件物品。

更高效的转化方法是:把第i种物品拆成费用为c[i]*2^k、价值为w[i]*2^k的若干件物品,其中k满足c[i]*2^k<V。这是二进制的思想,因为不管最优策略选几件第i种物品,总可以表示成若干个2^k件物品的和。这样把每种物品拆成O(log(V/c[i]))件物品,是一个很大的改进。但我们有更优的O(VN)的算法。 * O(VN)的算法这个算法使用一维数组,先看伪代码:

for i=1..N 
	for v=0..V 
		f[v]=max{f[v],f[v-c[i]]+w[i]};

你会发现,这个伪代码与P01的伪代码只有v的循环次序不同而已。为什么这样一改就可行呢?首先想想为什么P01中要按照v=V..0的逆序来循环。这是因为要保证第i次循环中的状态f[i][v]是由状态f[i-1][v-c[i]]递推而来。换句话说,这正是为了保证每件物品只选一次,保证在考虑“选入第i件物品”这件策略时,依据的是一个绝无已经选入第i件物品的子结果f[i-1][v-c[i]]。而现在完全背包的特点恰是每种物品可选无限件,所以在考虑“加选一件第i种物品”这种策略时,却正需要一个可能已选入第i种物品的子结果f[i][v-c[i]],所以就可以并且必须采用v= 0..V的顺序循环。这就是这个简单的程序为何成立的道理。

这个算法也可以以另外的思路得出。例如,基本思路中的状态转移方程可以等价地变形成这种形式:f[i][v]=max{f[i-1][v],f[i][v-c[i]]+w[i]},将这个方程用一维数组实现,便得到了上面的伪代码。

从小到大进行枚举的在计算f[v]的时候用的是v-v[i]的状态,v-v[i]的值是已经被计算过的,因为是从小到大进行枚举的,所以所有比v小的值都已经被计算过了v-v[i]也在里面,所以v-v[i]的值是已经被计算过的。也就是说f[v-v[i]]也就表示考虑前i个物品,包括第i个物品的情况下,体积是v-v[i]的时候,最大价值是多少那么,这里面就可能包含了若干个第i个物品

数学归纳法证明
1.假设考虑前i-1个物品之后,所有的f[v]都是正确的
2.来证明:考虑完第i个物品后,所有的f[v]也都是正确的

f[0]=0;显然成立
对于任意一个v而言,如果最优解中包含k个v[i];

在从小到大进行枚举的时候,一定会枚举到
f[v-k*v[i]]这个状态,而f[v-k*v[i]]一定会拿
f[v-k*v[i]-v[i]]+w[i]来更新它,因为最优解中包含k个v[i]
所以f[v-k*v[i]-v[i]]+w[i] 一定是不包含v[i]的
所以f[v-k*v[i]]就算完了
之后会算
f[v-(k-1)*v[i]]这个状态
f[v-(k-1)*v[i]]是由f[v-(k-1)*v[i]-v[i]]+w[i]
f[(k-1)*v[i]]这个状态会拿f[k*v[i]]来更新,而f[v-k*v[i]] 是一个v[i]都不包含的
枚举完一次
f[v-(k-1)*v[i]-v[i]]+w[i] 会包含一个v[i]
......
计算f[v]的时候会拿f[v-v[i]]+w[i]来更新
f[v-v[i]]+w[i]一定是计算过只包括k-1个物品状态的,和这个状态取最大值
而f[v]又加上了一个v[i]
所以f[v]一定枚举过包含了了k个v[i]的状态



总结

完全背包问题也是一个相当基础的背包问题,它有两个状态转移方程,分别在“基本思路”以及“O(VN)的算法“的小节中给出。希望你能够对这两个状态转移方程都仔细地体会,不仅记住,也要弄明白它们是怎么得出来的,最好能够自己想一种得到这些方程的方法。事实上,对每一道动态规划题目都思考其方程的意义以及如何得来,是加深对动态规划的理解、提高动态规划功力的好方法。

真题

有 N 种物品和一个容量是 V 的背包,每种物品都有无限件可用。

第 i 种物品的体积是 vi,价值是 wi。

求解将哪些物品装入背包,可使这些物品的总体积不超过背包容量,且总价值最大。 输出最大价值。

输入格式

第一行两个整数,N,V,用空格隔开,分别表示物品种数和背包容积。

接下来有 N行,每行两个整数 vi,wi,用空格隔开,分别表示第 i 种物品的体积和价值。

输出格式

输出一个整数,表示最大价值。

题解

/*
https://www.acwing.com/problem/content/3/
**/
#include<iostream>
#include<cstring>
#include<algorithm>
using namespace std;
const int N=1010;
int m,n;
int f[N];
int main(){
    cin>>n>>m;
    for(int i=0;i<n;i++){
        int v,w;
        cin>>v>>w;
        for(int j=v;j<=m;j++){
            f[j]=max(f[j],f[j-v]+w);
        }
    }
    cout<<f[m]<<endl;
    return 0;
}

我们发现本题仅仅与01背包问题的不同之处在于循环的顺序不同。而现在完全背包的特点恰是每种物品可选无限件,所以在考虑“加选一件第i种物品”这种策略时,却正需要一个可能已选入第i种物品的子结果f[i][v-c[i]],所以就可以并且必须采用v= 0..V的顺序循环。这就是这个简单的程序为何成立的道理。

P03: 多重背包问题

题目 有N种物品和一个容量为V的背包。第i种物品最多有n[i]件可用,每件费用是c[i],价值是w[i]。求解将哪些物品装入背包可使这些物品的费用总和不超过背包容量,且价值总和最大。

基本算法 这题目和完全背包问题很类似。基本的方程只需将完全背包问题的方程略微一改即可,因为对于第i种物品有n[i]+1种策略:取0件,取1件……取 n[i]件。令f[i][v]表示前i种物品恰放入一个容量为v的背包的最大权值,则:f[i][v]=max{f[i-1][v-k*c[i]]+ k*w[i]|0<=k<=n[i]}。复杂度是O(V*∑n[i])。

转化为01背包问题 另一种好想好写的基本方法是转化为01背包求解:把第i种物品换成n[i]件01背包中的物品,则得到了物品数为∑n[i]的01背包问题,直接求解,复杂度仍然是O(V*∑n[i])。

但是我们期望将它转化为01背包问题之后能够像完全背包一样降低复杂度。仍然考虑二进制的思想,我们考虑把第i种物品换成若干件物品,使得原问题中第i种物品可取的每种策略——取0..n[i]件——均能等价于取若干件代换以后的物品。另外,取超过n[i]件的策略必不能出现。

方法是:将第i种物品分成若干件物品,其中每件物品有一个系数,这件物品的费用和价值均是原来的费用和价值乘以这个系数。使这些系数分别为 1,2,4,…,2^(k-1),n[i]-2^k+1,且k是满足n[i]-2^k+1>0的最大整数。例如,如果n[i]为13,就将这种物品分成系数分别为1,2,4,6的四件物品。

分成的这几件物品的系数和为n[i],表明不可能取多于n[i]件的第i种物品。另外这种方法也能保证对于0..n[i]间的每一个整数,均可以用若干个系数的和表示,这个证明可以分0..2^k-1和2^k..n[i]两段来分别讨论得出,并不难,希望你自己思考尝试一下。

这样就将第i种物品分成了O(log n[i])种物品,将原问题转化为了复杂度为O(V*∑log n[i])的01背包问题,是很大的改进。

O(VN)的算法 多重背包问题同样有O(VN)的算法。这个算法基于基本算法的状态转移方程,但应用单调队列的方法使每个状态的值可以以均摊O(1)的时间求解。由于用单调队列优化的DP已超出了NOIP的范围,故本文不再展开讲解。我最初了解到这个方法是在楼天成的“男人八题”幻灯片上。

小结 这里我们看到了将一个算法的复杂度由O(V*∑n[i])改进到O(V*∑log n[i])的过程,还知道了存在应用超出NOIP范围的知识的O(VN)算法。希望你特别注意“拆分物品”的思想和方法,自己证明一下它的正确性,并用尽量简洁的程序来实现。

真题

有 N 种物品和一个容量是 M的背包。

第 i 种物品最多有 si 件,每件体积是 vi,价值是 wi。

求解将哪些物品装入背包,可使物品体积总和不超过背包容量,且价值总和最大。 输出最大价值。

输入格式

第一行两个整数,N,M用空格隔开,分别表示物品种数和背包容积。

接下来有 N,每行三个整数 vi,wi,si用空格隔开,分别表示第 i种物品的体积、价值和数量。

输出格式

输出一个整数,表示最大价值。

题解

三重循环

01背包问题只有选和不选两种选法,多重循环的话可以选0,1,2,3….s种选法

/*
https://www.acwing.com/problem/content/4/
**/
#include<iostream>
#include<cstring>
#include<algorithm>
using namespace std;
const int N=110;
int m,n;//n件物品   容量是m
int f[N];//f[i]:总体积是i的情况下最大价值是多少
int main(){
    cin>>n>>m;
    for(int i=0;i<n;i++){ // 循环物品
        int v,w,s;
        cin>>v>>w>>s;//体积、价值、数量
        for(int j=m;j>=v;j--){ // 循环体积 从大到小
            for(int k=1;k<=s&&k*v<=j;k++){ //循环决策
                f[j]=max(f[j],f[j-k*v]+k*w);
            }
        }
    }
    cout<<f[m]<<endl;
    return 0;
}
/**
最终结果是f[m]

1. f[i]=0     res = f[m];
2. f[0]=0;f[i]=-INF,i!=0
res = max(f[0...m])


k的范围是1....s   0的话就是不选  结果还是f[j]  没有实际的意义  就决定了j的范围是可以到v  而没有必要到0
j的范围是m....v   就决定了k一定是要选择一个 

如果K是从0开始的,那么j的范围就要包括0  那么j  m......0     k 0.....s

*/
  • 使用数组的时候一定要注意数组的边界问题

与01背包的相似之处

  • 01背包问题是这个物品只有选或不选两种状态
  • 多重背包问题是这个物品可以选0个 1个 2 3 ……….一直到s,总共s+1个状态

因此多重背包就多了一重循环,时间复杂度就变成了O(n^3),就需要进一步进行优化

优化方法有

  1. 二进制优化

换一种思路:不如把所有的物品都看作是一种独立的物品,重复的物品也看作是独立的,那么对于这种展开的物品来看又转换为了01背包问题。每一件物品只有选或不选两种状态。

给定一个数S,最少可以选择几个数,使得能够组成和为0….S

比如 7 答案是3 {1、 2、 4}

0 0
1 1
2 2
3 1+2
4 4
5 1+4
6 2+4
7 1+2+4

比如10 答案是4 {1、2、4、3}

最后一个数不能是8如果是8的话,就可以表示0-15内的所有的数字:我们可以用10-1-2-4=3

结论是 :log2(s)向下取整

那么对于重复的物品(比如有S份),我们就不需要把他拆分成S份了 可以拆分组成log2(S)份

二进制题解

/*
https://www.acwing.com/problem/content/5/
**/
#include<cstdio>
#include<iostream>
#include<cstring>
#include<vector>
#include<algorithm>

using namespace std;
const int N=2010;
int f[N];
int n,m;
struct Good{
  int v,w;  
};
int main(){
    vector<Good> goods;
    cin>>n>>m;
    for(int i=0;i<n;i++){
        int v,w,s;
        cin>>v>>w>>s;
        for(int k=1;k<=s;k*=2){
            s-=k;
            goods.push_back({v*k,w*k});
        }
        if(s>0)goods.push_back({v*s,w*s});
    }
    //转化成01背包问题
    for(auto good:goods){
        for(int j=m;j>=good.v;j--){
            f[j]=max(f[j],f[j-good.v]+good.w);
        }
    }
    cout<<f[m]<<endl;
    return 0;
}

终极版本

使用的单调队列的思想 leetcode 289题


/**
<<男人八题>>
单调队列问题
f[j] = f[j-v]+w,f[j-2*v]+2*w,...f[j-k*v]+k*w 取一个最大值
f[j+v]  把框往后移了一位  转移成了一个经典的单调队列问题:
给一个序列,动态的求出所有长度为k的框里的最大值
**/
#include<cstdio>
#include<iostream>
#include<cstring>
#include<algorithm>
#include<vector>
using namespace std;

const int N=20010;
int f[N],g[N],q[N];
int n,m;

int main(){
    cin>>n>>m;
    for(int i=0;i<n;i++){
        int c,w,s;
        cin>>c>>w>>s;
        memcpy(g,f,sizeof f);
        for(int j=0;j<c;j++){ //枚举余数、
            //每个余数是相互独立的
            int hh=0,tt=-1;
            for(int k=j;k<=m;k+=c){ // 余数中的所有数 使用队列进行优化
                f[k]=g[k];
                if(hh<=tt&&k-s*c>q[hh])hh++; //每次把队首取出来  ,队列的首部一定是最大的数字
                if(hh<=tt)f[k]=max(f[k],g[q[hh]]+(k-q[hh])/c*w);//用最大数更新当前的数
                while(hh<=tt&&g[q[tt]]-(q[tt]-j)/c*w<=g[k]-(k-j)/c*w)tt--;//每次把当前数往队列里面插,插的时候要剔出队列中一定不会用到的数
                q[++tt]=k;//把当前数加到队列里面去
            }
        }
    }
    cout<<f[m]<<endl; //f[m]就是答案
    return 0;
}

P04: 混合三种背包问题

问题 如果将P01、P02、P03混合起来。也就是说,有的物品只可以取一次(01背包),有的物品可以取无限次(完全背包),有的物品可以取的次数有一个上限(多重背包)。应该怎么求解呢?

01背包与完全背包的混合 考虑到在P01和P02中最后给出的伪代码只有一处不同,故如果只有两类物品:一类物品只能取一次,另一类物品可以取无限次,那么只需在对每个物品应用转移方程时,根据物品的类别选用顺序或逆序的循环即可,复杂度是O(VN)。伪代码如下:

for i=1..N
    if 第i件物品是01背包
        for v=V..0
        	f[v]=max{f[v],f[v-c[i]]+w[i]};
    else if 第i件物品是完全背包
    	for v=0..V
   			f[v]=max{f[v],f[v-c[i]]+w[i]};

再加上多重背包 如果再加上有的物品最多可以取有限次,那么原则上也可以给出O(VN)的解法:遇到多重背包类型的物品用单调队列解即可。但如果不考虑超过NOIP范围的算法的话,用P03中将每个这类物品分成O(log n[i])个01背包的物品的方法也已经很优了。

小结 有人说,困难的题目都是由简单的题目叠加而来的。这句话是否公理暂且存之不论,但它在本讲中已经得到了充分的体现。本来01背包、完全背包、多重背包都不是什么难题,但将它们简单地组合起来以后就得到了这样一道一定能吓倒不少人的题目。但只要基础扎实,领会三种基本背包问题的思想,就可以做到把困难的题目拆分成简单的题目来解决。

真题

有 N 种物品和一个容量是 V的背包。

物品一共有三类:

  • 第一类物品只能用1次(01背包);
  • 第二类物品可以用无限次(完全背包);
  • 第三类物品最多只能用 si 次(多重背包);

每种体积是 vi,价值是 wi。

求解将哪些物品装入背包,可使物品体积总和不超过背包容量,且价值总和最大。 输出最大价值。

输入格式

第一行两个整数,N,V,用空格隔开,分别表示物品种数和背包容积。

接下来有 NN 行,每行三个整数 vi,wi,si,用空格隔开,分别表示第 i 种物品的体积、价值和数量。

  • si=−1 表示第 i 种物品只能用1次;
  • si=0 表示第 i 种物品可以用无限次;
  • si>0 表示第 i 种物品可以使用 sisi 次;

输出格式

输出一个整数,表示最大价值。

题解

/*
https://www.acwing.com/problem/content/description/7/
**/
#include<iostream>
#include<cstring>
#include<algorithm>
#include<vector>
using namespace std;
const int N=1010;
int m,n;//n件物品   容量是m
int f[N];//f[i]:总体积是i的情况下最大价值是多少

struct Thing{
  int kind;// 类型 01 完全 多重
  int v,w;
};
vector<Thing> things;

int main(){
    cin>>n>>m;
    /**将问题分成两类 01 背包问题 完全背包问题*/
    for(int i=0;i<n;i++){
        int v,w,s;
        cin>>v>>w>>s;//体积、价值、数量
        if(s<0)things.push_back({-1,v,w}); //01背包问题
        else if(s==0)things.push_back({0,v,w}); //完全背包问题
        else{//多重背包问题  分成若干份 转换成01背包问题
            
            for(int k=1;k<=s;k*=2){//分成log(s)份
                s-=k;
                things.push_back({-1,v*k,w*k});
            }
            if(s>0)things.push_back({-1,v*s,w*s});
        }

    }
    for(auto thing:things){
        if(thing.kind<0){
            //01背包 需要从大到小进行枚举容量
            for(int j=m;j>=thing.v;j--)f[j]=max(f[j],f[j-thing.v]+thing.w);
            
        }else{
            //完全背包需要从大到小进行枚举
            for(int j=thing.v;j<=m;j++)f[j]=max(f[j],f[j-thing.v]+thing.w);
        }
    }
    cout<<f[m]<<endl;
    return 0;
}

判断当前物品是什么类型的,状态转移的时候按照对应的类型进行转移

P05: 二维费用的背包问题

问题 二维费用的背包问题是指:对于每件物品,具有两种不同的费用;选择这件物品必须同时付出这两种代价;对于每种代价都有一个可付出的最大值(背包容量)。问怎样选择物品可以得到最大的价值。设这两种代价分别为代价1和代价2,第i件物品所需的两种代价分别为a[i]和b[i]。两种代价可付出的最大值(两种背包容量)分别为V和U。物品的价值为w[i]。

算法 费用加了一维,只需状态也加一维即可。设f[i][v][u]表示前i件物品付出两种代价分别为v和u时可获得的最大价值。状态转移方程就是:f [i][v][u]=max{f[i-1][v][u],f[i-1][v-a[i]][u-b[i]]+w[i]}。如前述方法,可以只使用二维的数组:当每件物品只可以取一次时变量v和u采用顺序的循环,当物品有如完全背包问题时采用逆序的循环。当物品有如多重背包问题时拆分物品。

物品总个数的限制 有时,“二维费用”的条件是以这样一种隐含的方式给出的:最多只能取M件物品。这事实上相当于每件物品多了一种“件数”的费用,每个物品的件数费用均为1,可以付出的最大件数费用为M。换句话说,设f[v][m]表示付出费用v、最多选m件时可得到的最大价值,则根据物品的类型(01、完全、多重)用不同的方法循环更新,最后在f[0..V][0..M]范围内寻找答案。

另外,如果要求“恰取M件物品”,则在f[0..V][M]范围内寻找答案。

小结 事实上,当发现由熟悉的动态规划题目变形得来的题目时,在原来的状态中加一维以满足新的限制是一种比较通用的方法。希望你能从本讲中初步体会到这种方法。

真题

有 N 件物品和一个容量是 V 的背包,背包能承受的最大重量是 M。

每件物品只能用一次。体积是 vi,重量是 mi,价值是 wi。

求解将哪些物品装入背包,可使物品总体积不超过背包容量,总重量不超过背包可承受的最大重量,且价值总和最大。 输出最大价值。

输入格式

第一行两个整数,N,M,用空格隔开,分别表示物品件数、背包容积和背包可承受的最大重量。

接下来有 N 行,每行三个整数 vi,mi,wi,用空格隔开,分别表示第 i 件物品的体积、重量和价值。

输出格式

输出一个整数,表示最大价值。

题解

/*
https://www.acwing.com/problem/content/8/
每个物品只能用一次  01背包问题
**/
#include<iostream>
#include<cstring>
#include<algorithm>
#include<vector>
using namespace std;
const int N=110;
int n,v,m;//n件物品   容量是m
int f[N][N];//f[i]:总体积是i的情况下最大价值是多少

int main(){
    cin>>n>>v>>m;
    
    for(int i=0;i<n;i++){// 枚举物品
        int a,b,c;
        cin>>a>>b>>c;//体积、价值、数量
        for(int j=v;j>=a;j--){//枚举体积
            for(int k=m;k>=b;k--){//枚举重量
                f[j][k]=max(f[j][k],f[j-a][k-b]+c);
            }
        }
    }
    cout<<f[v][m]<<endl;
    return 0;
}

P06: 分组的背包问题

问题 有N件物品和一个容量为V的背包。第i件物品的费用是c[i],价值是w[i]。这些物品被划分为若干组,每组中的物品互相冲突,最多选一件。求解将哪些物品装入背包可使这些物品的费用总和不超过背包容量,且价值总和最大。

算法 这个问题变成了每组物品有若干种策略:是选择本组的某一件,还是一件都不选。也就是说设f[k][v]表示前k组物品花费费用v能取得的最大权值,则有f[k][v]=max{f[k-1][v],f[k-1][v-c[i]]+w[i]|物品i属于第k组}。

使用一维数组的伪代码如下:

for 所有的组k
	for 所有的i属于组k
		for v=V..0
			f[v]=max{f[v],f[v-c[i]]+w[i]}

另外,显然可以对每组中的物品应用P02中“一个简单有效的优化”。

小结 分组的背包问题将彼此互斥的若干物品称为一个组,这建立了一个很好的模型。不少背包问题的变形都可以转化为分组的背包问题(例如P07),由分组的背包问题进一步可定义“泛化物品”的概念,十分有利于解题。

真题

有 N组物品和一个容量是 V 的背包。

每组物品有若干个,同一组内的物品最多只能选一个。 每件物品的体积是 vij,价值是 wij,其中 i是组号,j 是组内编号。

求解将哪些物品装入背包,可使物品总体积不超过背包容量,且总价值最大。

输出最大价值。

输入格式

第一行有两个整数 N,M,V,用空格隔开,分别表示物品组数和背包容量。

接下来有 N 组数据:

  • 每组数据第一行有一个整数 Si,表示第 i个物品组的物品数量;
  • 每组数据接下来有 Si 行,每行有两个整数 vij,wij,用空格隔开,分别表示第 i 个物品组的第 j 个物品的体积和价值;

输出格式

输出一个整数,表示最大价值。

题解

多重背包问题实际上是分组背包问题的一种特殊情况

/*
https://www.acwing.com/problem/content/9/
f[i][j] 前i组物品体积是j的情况下的最大价值
有物品组的概念的话就意味着,假设这组物品有s个的话,它的决策就有s+1个 不选 选1个 选2个..
**/
#include<iostream>
#include<cstring>
#include<algorithm>
using namespace std;
const int N=110;
int n,m;//n件物品   容量是m
int f[N],v[N],w[N];//f[i]:总体积是i的情况下最大价值是多少
int main(){
    cin>>n>>m;
    for(int i=0;i<n;i++){
        int s;
        cin>>s;
        for(int j=0;j<s;j++)cin>>v[j]>>w[j];
        for(int j=m;j>=0;j--){
            for(int k=0;k<s;k++){
                if(j>=v[k])f[j]=max(f[j],f[j-v[k]]+w[k]);
            }
        }
    }
    cout<<f[m]<<endl;
    return 0;
}

P07: 有依赖的背包问题

简化的问题 这种背包问题的物品间存在某种“依赖”的关系。也就是说,i依赖于j,表示若选物品i,则必须选物品j。为了简化起见,我们先设没有某个物品既依赖于别的物品,又被别的物品所依赖;另外,没有某件物品同时依赖多件物品。

算法 这个问题由NOIP2006金明的预算方案一题扩展而来。遵从该题的提法,将不依赖于别的物品的物品称为“主件”,依赖于某主件的物品称为“附件”。由这个问题的简化条件可知所有的物品由若干主件和依赖于每个主件的一个附件集合组成。

按照背包问题的一般思路,仅考虑一个主件和它的附件集合。可是,可用的策略非常多,包括:一个也不选,仅选择主件,选择主件后再选择一个附件,选择主件后再选择两个附件……无法用状态转移方程来表示如此多的策略。(事实上,设有n个附件,则策略有2^n+1个,为指数级。)

考虑到所有这些策略都是互斥的(也就是说,你只能选择一种策略),所以一个主件和它的附件集合实际上对应于P06中的一个物品组,每个选择了主件又选择了若干个附件的策略对应于这个物品组中的一个物品,其费用和价值都是这个策略中的物品的值的和。但仅仅是这一步转化并不能给出一个好的算法,因为物品组中的物品还是像原问题的策略一样多。

再考虑P06中的一句话:可以对每组中的物品应用P02中“一个简单有效的优化”。这提示我们,对于一个物品组中的物品,所有费用相同的物品只留一个价值最大的,不影响结果。所以,我们可以对主件i的“附件集合”先进行一次01背包,得到费用依次为0..V-c[i]所有这些值时相应的最大价值f’[0..V-c[i]]。那么这个主件及它的附件集合相当于V-c[i]+1个物品的物品组,其中费用为c[i]+k的物品的价值为f’[k]+w[i]。也就是说原来指数级的策略中有很多策略都是冗余的,通过一次01背包后,将主件i转化为 V-c[i]+1个物品的物品组,就可以直接应用P06的算法解决问题了。

更一般的问题 更一般的问题是:依赖关系以图论中“森林”的形式给出(森林即多叉树的集合),也就是说,主件的附件仍然可以具有自己的附件集合,限制只是每个物品最多只依赖于一个物品(只有一个主件)且不出现循环依赖。

解决这个问题仍然可以用将每个主件及其附件集合转化为物品组的方式。唯一不同的是,由于附件可能还有附件,就不能将每个附件都看作一个一般的01 背包中的物品了。若这个附件也有附件集合,则它必定要被先转化为物品组,然后用分组的背包问题解出主件及其附件集合所对应的附件组中各个费用的附件所对应的价值。

事实上,这是一种树形DP,其特点是每个父节点都需要对它的各个儿子的属性进行一次DP以求得自己的相关属性。这已经触及到了“泛化物品”的思想。看完P08后,你会发现这个“依赖关系树”每一个子树都等价于一件泛化物品,求某节点为根的子树对应的泛化物品相当于求其所有儿子的对应的泛化物品之和。

小结 NOIP2006的那道背包问题我做得很失败,写了上百行的代码,却一分未得。后来我通过思考发现通过引入“物品组”和“依赖”的概念可以加深对这题的理解,还可以解决它的推广问题。用物品组的思想考虑那题中极其特殊的依赖关系:物品不能既作主件又作附件,每个主件最多有两个附件,可以发现一个主件和它的两个附件等价于一个由四个物品组成的物品组,这便揭示了问题的某种本质。

我想说:失败不是什么丢人的事情,从失败中全无收获才是。

真题

有 N个物品和一个容量是 M 的背包。

物品之间具有依赖关系,且依赖关系组成一棵树的形状。如果选择一个物品,则必须选择它的父节点。

如下图所示: QQ图片20181018170337.png

如果选择物品5,则必须选择物品1和2。这是因为2是5的父节点,1是2的父节点。

每件物品的编号是 i,体积是 vi,价值是 wi,依赖的父节点编号是 pi。物品的下标范围是 1…N1…N。

求解将哪些物品装入背包,可使物品总体积不超过背包容量,且总价值最大。

输出最大价值。

输入格式

第一行有两个整数 N,M,用空格隔开,分别表示物品个数和背包容量。

接下来有 NN 行数据,每行数据表示一个物品。 第 ii 行有三个整数 vi,wi,pi,用空格隔开,分别表示物品的体积、价值和依赖的物品编号。 如果 pi=−1,表示根节点。 数据保证所有物品构成一棵树。

输出格式

输出一个整数,表示最大价值。

题解

背包问题 和 树形dp

#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
const int N=110;
int n,m;
int h[N],e[N],ne[N],idx;
int v[N],w[N],f[N][N];// f[i][j]节点是i体积是j的情况下的体积最大是多少
void add(int a,int b){
    e[idx]=b,ne[idx]=h[a],h[a]=idx++;
}
/**背包问题都是
1. 循环物品
2. 循环体积
3. 循环决策

从上往下递归来做,每计算一个节点的时候先把子节点算出来,每个子节点在不同体积下的最大价值是多少
分组背包问题,每个节点都是一个组,不同体积代表不同组,每个组中只能选一个

和01背包有一点区别就是必须要选择物品
*/
void dfs(int u){
    for(int i=h[u];i!=-1;i=ne[i]){//循环物品组
        int son = e[i];
        dfs(son);//先去遍历子节点
        for(int j=m-v[u];j>=0;j--){//循环体积  从大到小进行遍历  01 背包问题  必须选当前的物品所以把当前物品的体积空出来
            for(int k=0;k<=j;k++){//循环物品组种选择哪一件物品
                f[u][j]=max(f[u][j],f[u][j-k]+f[son][k]);
            }
        }
    }
    for(int i=m;i>=v[u];i--)f[u][i]=f[u][i-v[u]]+w[u]; //如果体积大于等于当前物品的体积,需要在我们之前空出来的体积,把当前物品加进去
    for(int i=0;i<v[u];i++)f[u][i]=0;//如果体积小于当前物品的体积,意味着以当前节点为根节点的整颗子树都是不能选择的
}
int main(){
    memset(h,-1,sizeof h);
    cin>>n>>m;
    int root;
    for(int i=1;i<=n;i++){
        int p;
        cin>>v[i]>>w[i]>>p;
        if(p==-1){
            root=i;
        }else{
            add(p,i);
        } 
    }
    dfs(root);
    cout<<f[root][m]<<endl;
    return 0;
}

P08: 泛化物品

定义 考虑这样一种物品,它并没有固定的费用和价值,而是它的价值随着你分配给它的费用而变化。这就是泛化物品的概念。

更严格的定义之。在背包容量为V的背包问题中,泛化物品是一个定义域为0..V中的整数的函数h,当分配给它的费用为v时,能得到的价值就是h(v)。

这个定义有一点点抽象,另一种理解是一个泛化物品就是一个数组h[0..V],给它费用v,可得到价值h[V]。

一个费用为c价值为w的物品,如果它是01背包中的物品,那么把它看成泛化物品,它就是除了h©=w其它函数值都为0的一个函数。如果它是完全背包中的物品,那么它可以看成这样一个函数,仅当v被c整除时有h(v)=v/c*w,其它函数值均为0。如果它是多重背包中重复次数最多为n的物品,那么它对应的泛化物品的函数有h(v)=v/c*w仅当v被c整除且v/c<=n,其它情况函数值均为0。

一个物品组可以看作一个泛化物品h。对于一个0..V中的v,若物品组中不存在费用为v的的物品,则h(v)=0,否则h(v)为所有费用为v的物品的最大价值。P07中每个主件及其附件集合等价于一个物品组,自然也可看作一个泛化物品。

泛化物品的和 如果面对两个泛化物品h和l,要用给定的费用从这两个泛化物品中得到最大的价值,怎么求呢?事实上,对于一个给定的费用v,只需枚举将这个费用如何分配给两个泛化物品就可以了。同样的,对于0..V的每一个整数v,可以求得费用v分配到h和l中的最大价值f(v)。也即f(v)=max{h(k) +l(v-k)|0<=k<=v}。可以看到,f也是一个由泛化物品h和l决定的定义域为0..V的函数,也就是说,f是一个由泛化物品h和 l决定的泛化物品。

由此可以定义泛化物品的和:h、l都是泛化物品,若泛化物品f满足f(v)=max{h(k)+l(v-k)|0<=k<=v},则称f是h与l的和,即f=h+l。这个运算的时间复杂度是O(V^2)。

泛化物品的定义表明:在一个背包问题中,若将两个泛化物品代以它们的和,不影响问题的答案。事实上,对于其中的物品都是泛化物品的背包问题,求它的答案的过程也就是求所有这些泛化物品之和的过程。设此和为s,则答案就是s[0..V]中的最大值。

背包问题的泛化物品 一个背包问题中,可能会给出很多条件,包括每种物品的费用、价值等属性,物品之间的分组、依赖等关系等。但肯定能将问题对应于某个泛化物品。也就是说,给定了所有条件以后,就可以对每个非负整数v求得:若背包容量为v,将物品装入背包可得到的最大价值是多少,这可以认为是定义在非负整数集上的一件泛化物品。这个泛化物品——或者说问题所对应的一个定义域为非负整数的函数——包含了关于问题本身的高度浓缩的信息。一般而言,求得这个泛化物品的一个子域(例如0..V)的值之后,就可以根据这个函数的取值得到背包问题的最终答案。

综上所述,一般而言,求解背包问题,即求解这个问题所对应的一个函数,即该问题的泛化物品。而求解某个泛化物品的一种方法就是将它表示为若干泛化物品的和然后求之。

小结 本讲可以说都是我自己的原创思想。具体来说,是我在学习函数式编程的 Scheme 语言时,用函数编程的眼光审视各类背包问题得出的理论。这一讲真的很抽象,也许在“模型的抽象程度”这一方面已经超出了NOIP的要求,所以暂且看不懂也没关系。相信随着你的OI之路逐渐延伸,有一天你会理解的。

我想说:“思考”是一个OIer最重要的品质。简单的问题,深入思考以后,也能发现更多。

P09: 背包问题问法的变化

以上涉及的各种背包问题都是要求在背包容量(费用)的限制下求可以取到的最大价值,但背包问题还有很多种灵活的问法,在这里值得提一下。但是我认为,只要深入理解了求背包问题最大价值的方法,即使问法变化了,也是不难想出算法的。

例如,求解最多可以放多少件物品或者最多可以装满多少背包的空间。这都可以根据具体问题利用前面的方程求出所有状态的值(f数组)之后得到。

还有,如果要求的是“总价值最小”“总件数最小”,只需简单的将上面的状态转移方程中的max改成min即可。

下面说一些变化更大的问法。

输出方案 一般而言,背包问题是要求一个最优值,如果要求输出这个最优值的方案,可以参照一般动态规划问题输出方案的方法:记录下每个状态的最优值是由状态转移方程的哪一项推出来的,换句话说,记录下它是由哪一个策略推出来的。便可根据这条策略找到上一个状态,从上一个状态接着向前推即可。

还是以01背包为例,方程为f[i][v]=max{f[i-1][v],f[i-1][v-c[i]]+w[i]}。再用一个数组g[i] [v],设g[i][v]=0表示推出f[i][v]的值时是采用了方程的前一项(也即f[i][v]=f[i-1][v]),g[i][v]表示采用了方程的后一项。注意这两项分别表示了两种策略:未选第i个物品及选了第i个物品。那么输出方案的伪代码可以这样写(设最终状态为f[N][V]):

i=N
v=V
while(i>0)
    if(g[i][v]==0)
    	print "未选第i项物品"
    else if(g[i][v]==1)
    	print "选了第i项物品"
    	v=v-c[i]

另外,采用方程的前一项或后一项也可以在输出方案的过程中根据f[i][v]的值实时地求出来,也即不须纪录g数组,将上述代码中的g[i] [v]==0改成f[i][v]==f[i-1][v]g[i][v]==1改成f[i][v]==f[i-1][v-c[i]]+w[i]也可。

输出字典序最小的最优方案 这里“字典序最小”的意思是1..N号物品的选择方案排列出来以后字典序最小。以输出01背包最小字典序的方案为例。

一般而言,求一个字典序最小的最优方案,只需要在转移时注意策略。首先,子问题的定义要略改一些。我们注意到,如果存在一个选了物品1的最优方案,那么答案一定包含物品1,原问题转化为一个背包容量为v-c[1],物品为2..N的子问题。反之,如果答案不包含物品1,则转化成背包容量仍为V,物品为2..N的子问题。不管答案怎样,子问题的物品都是以i..N而非前所述的1..i的形式来定义的,所以状态的定义和转移方程都需要改一下。但也许更简易的方法是先把物品逆序排列一下,以下按物品已被逆序排列来叙述。

在这种情况下,可以按照前面经典的状态转移方程来求值,只是输出方案的时候要注意:从N到1输入时,如果f[i][v]==f[i-v]及f[i][v]==f[i-1][f-c[i]]+w[i]同时成立,应该按照后者(即选择了物品i)来输出方案。

求方案总数 对于一个给定了背包容量、物品费用、物品间相互关系(分组、依赖等)的背包问题,除了再给定每个物品的价值后求可得到的最大价值外,还可以得到装满背包或将背包装至某一指定容量的方案总数。

对于这类改变问法的问题,一般只需将状态转移方程中的max改成sum即可。例如若每件物品均是01背包中的物品,转移方程即为f[i][v]=sum{f[i-1][v],f[i-1][v-c[i]]+w[i]},初始条件f[0][0]=1

事实上,这样做可行的原因在于状态转移方程已经考察了所有可能的背包组成方案。

最优方案的总数 这里的最优方案是指物品总价值最大的方案。还是以01背包为例。

结合求最大总价值和方案总数两个问题的思路,最优方案的总数可以这样求:f[i][v]意义同前述,g[i][v]表示这个子问题的最优方案的总数,则在求f[i][v]的同时求g[i][v]的伪代码如下:

for i=1..N
	for v=0..V
        f[i][v]=max{f[i-1][v],f[i-1][v-c[i]]+w[i]}
        g[i][v]=0
        if(f[i][v]==f[i-1][v])
        	inc(g[i][v],g[i-1][v]
        if(f[i][v]==f[i-1][v-c[i]]+w[i])
        	inc(g[i][v],g[i-1][v-c[i]])

如果你是第一次看到这样的问题,请仔细体会上面的伪代码。

小结 显然,这里不可能穷尽背包类动态规划问题所有的问法。甚至还存在一类将背包类动态规划问题与其它领域(例如数论、图论)结合起来的问题,在这篇论背包问题的专文中也不会论及。但只要深刻领会前述所有类别的背包问题的思路和状态转移方程,遇到其它的变形问法,只要题目难度还属于NOIP,应该也不难想出算法。

触类旁通、举一反三,应该也是一个OIer应有的品质吧。

P10背包问题求方案数

真题

有 N 件物品和一个容量是 M 的背包。每件物品只能使用一次。

第 i 件物品的体积是 vi,价值是 wi。

求解将哪些物品装入背包,可使这些物品的总体积不超过背包容量,且总价值最大。

输出 最优选法的方案数。注意答案可能很大,请输出答案模 10^9+7 的结果。

输入格式

第一行两个整数,N,M,用空格隔开,分别表示物品数量和背包容积。

接下来有 N 行,每行两个整数 vi,wi用空格隔开,分别表示第 i 件物品的体积和价值。

输出格式

输出一个整数,表示 方案数 模 10^9+7 的结果。

题解

#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
const int N=1010,mod=1000000007,INF=1000000;
int n,m;
int f[N],g[N];//f[j]:体积恰好是j的情况下的最大价值是多少   g[j]:体积恰好是j的情况下,方案数是多少
int main(){
    cin>>n>>m;
    g[0]=1;
    for(int i=1;i<=m;i++)f[i]=-INF;//数据初始化  保证:所有的数据都是从0开始计数
    for(int i=0;i<n;i++){//循环物品
        int v,w;
        cin>>v>>w;
        for(int j=m;j>=v;j--){//从大到小循环体积
            int t=max(f[j],f[j-v]+w);//选或不选最大价值是多少
            int s=0;
            if(t==f[j])s+=g[j];
            if(t==f[j-v]+w)s+=g[j-v];
            if(s>=mod)s-=mod;
            f[j]=t;
            g[j]=s;
        }
    }
    int maxw =0;
    for(int i=0;i<=m;i++)maxw=max(maxw,f[i]);//求解最优解
    int res =0;
    for(int i=0;i<=m;i++){
        if(maxw==f[i]){//把所有等于最优解的方案累加
            res+=g[i];
            if(res>=mod)res-=mod;
        }
    }
    cout<<res<<endl;
    return 0;
}

P11背包问题求具体方案

真题

有 N 件物品和一个容量是 M的背包。每件物品只能使用一次。

第 i件物品的体积是 vi,价值是 wi。

求解将哪些物品装入背包,可使这些物品的总体积不超过背包容量,且总价值最大。

输出 字典序最小的方案。这里的字典序是指:所选物品的编号所构成的序列。物品的编号范围是 1…N1…N。

输入格式

第一行两个整数,N,M,用空格隔开,分别表示物品数量和背包容积。

接下来有 NN 行,每行两个整数 vi,wi,用空格隔开,分别表示第 i 件物品的体积和价值。

输出格式

输出一行,包含若干个用空格隔开的整数,表示最优解中所选物品的编号序列,且该编号序列的字典序最小。

物品编号范围是 1…N。

题解

#include<iostream>
#include<cstring>
#include<cstdio>
#include<algorithm>
using namespace std;
const int N=1010;
int n,m;
int v[N],w[N],f[N][N];
int main(){
    cin>>n>>m;
    for(int i=1;i<=n;i++)cin>>v[i]>>w[i];
    for(int i=n;i>=1;i--){// 每次看,从前往后看,当前这个物品能不能选,能选就一定要选。为了从前往后看,枚举物品从后往前进行枚举,保证字典序最小 
        for(int j=0;j<=m;j++){
            f[i][j]=f[i+1][j];
            if(j>=v[i])f[i][j]=max(f[i][j],f[i+1][j-v[i]]+w[i]);
        }
    }
    int vol = m;
    for(int i=1;i<=n;i++){ //从前往后看每个物品
        if(f[i][vol]==f[i+1][vol-v[i]]+w[i]){ //n
            cout<<i<<' ';
            vol-=v[i];
        }
    }
 return 0;
}

其他算法

位运算

不用加号的加法 不用乘号的乘法
对于Java来说,还有原子类这种包装类型的数据,也不失为一种好的办法
对于其他的语言来说,就只能借助位运算来进行加法的操作,对于加法来说不进位的加法是一种很特殊的加法,与亦或操作是一样的,
乘法的底层使用的就是加法,a*b 可以看作b个a相加
使用递归的思想:每次返回的结果都是一个常量即 f(a,b)==a+f(a,b-1)

首先看十进制是如何做的: 5+7=12,三步走

第一步:相加各位的值,不算进位,得到2。 第二步:计算进位值,得到10. 如果这一步的进位值为0,那么第一步得到的值就是最终结果。 第三步:重复上述两步,只是相加的值变成上述两步的得到的结果2和10,得到12。 同样我们可以用三步走的方式计算二进制值相加: 5—101,7—111

第一步:相加各位的值,不算进位,得到010,二进制每位相加就相当于各位做异或操作,101^111。 第二步:计算进位值,得到1010,相当于各位进行与操作得到101,再向左移一位得到1010,(101&111)<<1。 第三步重复上述两步,各位相加 010^1010=1000,进位值为100=(010 & 1010)<<1。 继续重复上述两步:1000^100 = 1100,进位值为0,跳出循环,1100为最终结果。 结束条件:进位为0,即a为最终的求和结果。

全排列问题

P01 不含重复元素

给定一个 没有重复 数字的序列,返回其所有可能的全排列。

public IList<IList<int>> Permute(int[] nums)
{
    IList<IList<int>> ans = new List<IList<int>>(); //存放所有解
    if (nums == null || nums.Length <= 0)return ans;
    /*   方式一 采用空间换时间的方法进行  标记
                bool[] marked = new bool[nums.Length];//进行状态标记
                backtrack_state(nums, marked, new List<int>(), ans);
                */


    /*
                 2. 采用交换的方式进行全排列    要使用回溯的思想
                 */
    DFS(nums, 0, ans);
    return ans;


}
static public void backtrack_state(int[] nums, bool[] marked, List<int> path, IList<IList<int>> ans)
{
    // 有效路径
    if (path.Count == nums.Length)
    {
        ans.Add(new List<int>(path)); // 拷贝操作,因为path再回溯上层会做修改
        return;
    }

    for (int i = 0; i < nums.Length; i++)
    {
        // 其实可以用状态来标记的,但是会占用空间复杂度
        if (marked[i]) continue;

        marked[i] = true;
        // 做选择
        path.Add(nums[i]);

        // 进入下一层决策树
        backtrack_state(nums, marked, path, ans);

        // 取消选择,
        marked[i] = false; // 状态重置
        path.RemoveAt(path.Count - 1);
    }
}
private void DFS(int[] nums, int pos, IList<IList<int>> result)
{
    if (pos >= nums.Length)
    {
        result.Add(new List<int>(nums));
    }

    for (int i = pos; i < nums.Length; ++i)
    {
        Swap(nums, i, pos);//交换
        DFS(nums, pos + 1, result);//下一层
        Swap(nums, i, pos);//回溯
    }
}

private void Swap(int[] nums, int i, int j)
{
    if (i != j)
    {
        int t = nums[i];
        nums[i] = nums[j];
        nums[j] = t;
    }
}

P02 含重复元素

public IList<IList<int>> PermuteUnique(int[] nums)
{
    IList<IList<int>> res = new List<IList<int>>();
    int len = nums.Length;
    if (nums == null || len <= 0) return res;
    /*方法一
    * List<int> item = new List<int>();
    bool[] used = new bool[len];
    Array.Sort(nums);// 排序(升序或者降序都可以),排序是剪枝的前提
    Dfs(nums, len, 0, used, item, res);
    */
    /**方式二*/
    bool flag = false;
    Perm(nums, 0, res,flag);
    return res;
}
private void Perm(int[] nums, int pos, IList<IList<int>> result,bool flag)
{
    if (pos == nums.Length)
    {
        result.Add(new List<int>(nums));
    }
    for (int i = pos; i < nums.Length; ++i)
    {
        for(int k = pos; k < i; k++)//通过判断之前的元素是否包含当前元素,包含则不交换
        {
            if (nums[k] == nums[i])
            {
                flag = true;
                break;
            }
        }
        if (flag)
        {
            flag = false;
            continue;
        }
        Swap(nums, i, pos);//交换
        Perm(nums, pos + 1, result,flag);//下一层
        Swap(nums, i, pos);//回溯
    }
}

private void Swap(int[] nums, int i, int j)
{
    if (i != j)
    {
        int t = nums[i];
        nums[i] = nums[j];
        nums[j] = t;
    }
}

public void Dfs(int[] nums, int len, int depth, bool[] used, List<int> item, List<IList<int>> res)
{
    if (depth == len)
    {
        res.Add(new List<int>(item));
        return;
    }
    for (int i = 0; i < len; i++)
    {
        if (used[i])
            continue;
        // 剪枝条件:i > 0 是为了保证 nums[i - 1] 有意义
        // 写 !used[i - 1] 是因为 nums[i - 1] 在深度优先遍历的过程中刚刚被撤销选择
        if (i > 0 && nums[i] == nums[i - 1] && used[i - 1] == false)
        {
            continue;
        }
        item.Add(nums[i]);
        depth++;
        used[i] = true;
        Dfs(nums, len, depth, used, item, res);
        // 回溯部分的代码,和 dfs 之前的代码是对称的
        item.RemoveAt(item.Count - 1);
        depth--;
        used[i] = false;
    }
}