一、绪论
1、数据结构概论
数据结构研究计算机的操作对象以及他们之间的关系和操作。
2、算法的定义、特征以及要求
算法:是对特定问题求解步骤的一种描述,它是指令的有限序列,是一系列输入转化为输出的计算步骤。
算法的特征:输入、输出、有穷性、确定性、可行性。
算法的设计要求:正确性、可读性、健壮性、效率与低存储量需求。
3、算法复杂度
通常我们用时间复杂度和空间复杂度来衡量一个算法的优劣。
3.1 时间复杂度
从时间的维度上对算法进行衡量,忽略程序具体的运行时间,从算法整体方面去考虑 ,通常使用:「 大O符号表示法 」,即 T(n) = O(f(n))
。利用 大O符号有三个作用:限制忽略数学公式中低阶项产生的误差;限制由于忽略程序中对运行时间贡献小的部分产生的错误;允许我们按照算法总运行时间的上界对算法进行分类。
常见的时间复杂度量级有:常数阶、对数阶、线性阶、线性对数阶、平方阶、立方阶、K次方阶、指数阶
3.2 空间复杂度
空间复杂度是对一个算法在运行过程中临时占用存储空间大小的一个量度,同样反映的是一个趋势,我们用 S(n) = O(f(n))
来定义。
常见的空间复杂度量级有:、、
二、线性表
1、顺序存储结构
逻辑上相邻的数据元素,在物理上也是相邻的。
1.1 修改——修改第 i i i个位置的元素
核心语句:
data[i]=value; //将数组data中索引为i的元素修改为value
时间复杂度:
1.2 插入——在第 i i i个位置前插入一个元素
核心语句:
for(int j=n;j>=i;j--)
{
data[j+1]=data[j];
}
data[i]=value;
n++;
时间复杂度:
1.3 删除——删除第 i i i个位置的元素
核心代码:
for(int j=i;j<n;j++)
{
data[j]=data[j+1];
}
n--;
时间复杂度:
2、链式存储结构
逻辑上相邻的数据元素,在物理上不一定相邻。
定义节点:
class Node{
int val;
Node next;
Node(int val){
this.val = val;
next=null;
}
}
创建链表:
// 头插法创建链表
public static Node creat_link_head(Node head,int[] data){
for(int i=0;i<data.length;i++){
Node p = new Node(i);
p.next = head.next;
head.next = p;
}
return head;
}
// 尾插法创建链表
public static Node creat_link_tail(Node head,int[] data){
Node tail = head;
for(int i=0;i<data.length;i++){
Node p = new Node(i);
tail.next = p;
tail = tail.next;
}
return head;
}
遍历链表:
public static void link_travel(Node head){
Node p = head.next;
while (p!=null){
System.out.print(p.val+" ");
p = p.next;
}
}
2.1 链式存储结构——修改
核心代码:
public static Node link_updata(Node head,int i,int x){
Node p = head.next;
int j = 0;
while (p!=null){
if(j == i){
//更新i结点的元素
p.val = x;
}
j++;
p = p.next;
}
return head;
}
2.2 链式存储结构——插入
核心代码:
public static Node link_insert(Node head,int i,int x){
Node p = head;
int j = 0;
while (p!=null){
if(j==i){
// 在p的后面插入q节点
Node q = new Node(x);
q.next = p.next;
p.next = q;
return head;
}
p = p.next;
j++;
}
return head;
}
2.3 链式存储结构——删除
核心代码:
public static int link_delete(Node head,int i){
Node p = head;
int j=0;
while (p.next!=null){
if(j==i){
//删除该结点元素
Node q = p.next;
p.next = q.next;
return q.val; //无需free(q),java会自动回收
}
p = p.next;
j++;
}
return -1; //i超出了链表的长度,删除失败
}
三、栈和队列
1、栈的定义及操作
栈是只准在一端进行插入和删除操作的线性表,该端称为顶端。
入栈:插入元素到栈顶的操作,堆栈指针“先压后加”
出栈:从栈顶删除最后一个元素的操作,堆栈指针“先减后弹”
1.1 顺序存储的栈
p指向栈顶!
class Stack{
int[] value;
int length;
int p;
Stack(int length){
this.length = length;
value=new int[length];
p = 0;
}
//判空条件
public boolean isEmpty(){
return p == 0;
}
//栈满条件
public boolean isFull(){
return p == length;
}
public void push(int val){
if(!isFull()){
value[p++]=val;
}
}
public int pop(){
if(!isEmpty()){
return value[--p];
}
return -1; // 栈溢出,出栈失败
}
}
1.2 链式存储的栈
p指向栈顶!
class Stack_link{
Node head;
private Node p;
int length;
Stack_link(int length){
this.length = length;
head = new Node(-1);
p = head;
for(int i=1;i<length;i++){
Node q = new Node(-1);
p.next = q;
p = p.next;
}
p = head;
}
//判空条件
public boolean isEmpty(){
return p==head;
}
//栈满条件
public boolean isFull(){
if(p == null)return true;
else return false;
}
public void push(int val){
if(!isFull()){
p.val = val;
p = p.next;
}
}
public int pop(){
if (!isEmpty()){
Node q = p;
p = head;
while (p.next != q){
p = p.next;
}
return p.val;
}
else return -1;
}
}
2、队列的定义及操作
队列的删除在一端(队首),插入则在另一段(队尾),所以在队列中需要队首、队尾两个指针。
入队:从队尾添加元素,遵循“先加再入队”
出队:从队首删除元素,遵循“先加再出队”
2.1 顺序存储的循环队列
class Queue{
int[] value;
int Max;
int front;
int rear;
Queue(int Max){
this.Max = Max;
value = new int[Max];
front = 0;
rear = 0;
}
public boolean isEmpty(){
return front==rear;
}
public boolean isFull(){
return (rear+1)%Max == front; //空一个位置
}
public void EnQueue(int val){
if(!isFull()){
rear = (rear+1)%Max;
value[rear] = val;
}
}
public int deleQueue(){
if(!isEmpty()){
front = (front+1)%Max;
return value[front];
}
return -1;
}
public int length(){
return (Max+rear-front)%Max;
}
}
2.2 链式存储的队列
class Queue_link{
Node head;
int length;
Node rear;
Node front;
int Max;
Queue_link(int max){
head = new Node(-1); //头结点
rear = head;
front = head;
length = 0;
Max = max;
}
public boolean isEmpty(){
return front == rear;
}
//链表存储的队列,理论上不会满,人为限制最大Max
public boolean isFull(){
return length == Max;
}
public void EnQueue(int val){
if(!isFull()){
Node q = new Node(val);
rear.next = q;
rear = rear.next;
length++;
}
}
public int deleQueue(){
if(!isEmpty()){
Node q = front.next;
front.next = q.next;
length--;
return q.val;
}
return -1;
}
public int length(){
return length;
}
}
2.3 链式存储的循环队列
class Queue_round{
Node head;
int Max;
Node front;
Node rear;
int length;
// boolean isEmpty_flag=true;
Queue_round(int max){
this.Max = max;
length = 0;
head = new Node(-1);
Node p = head;
for(int i=1;i<max;i++){
Node q = new Node(-1);
p.next = q;
p = p.next;
}
p.next = head; // 成环
front = head;
rear = head;
}
public boolean isEmpty(){
return front==rear;
}
public boolean isFull(){
// 一共三种办法
// 1.计数法,记录长度
// return length==Max;
// 2.标记法,front==rear有两种情况
// if(isEmpty_flag && front==rear)return true;
// else return false;
//3.空一个存储位置(常用)
return rear.next == front;
}
public void EnQueue(int val){
if(!isFull()){
rear = rear.next;
rear.val = val;
length++;
}
}
public int deleQueue(){
if(!isEmpty()){
front = front.next;
length--;
return front.val;
}
return -1;
}
public int length(){
return length;
}
四、树和二叉树
1、二叉树的性质
- 二叉树的第层上至多有个结点()
- 深度为的二叉树至多有个结点
- 任何一个二叉树,度为2的节点数和度为0的叶子结点之间的关系为:
- 具有个结点的完全二叉树深度为
- 对完全二叉树用编号,对于节点,其左子为,右子为,其双亲为,同理最后一个非叶子节点编号为,
2、树和二叉树的相互转换
树转化为二叉树:
二叉树转化为树:
3、树和森林的相互转换
森林转化为二叉树:
二叉树还原为森林:
4、树的存储方法
4.1 左孩子右孩子表示法(二叉树)
例子:
4.2 双亲表示法
例子:
4.3 孩子表示法
4.4 孩子兄弟表示法
5、二叉树的遍历
5.1 构建二叉树
通过一个先序的二维数组的形式构造二叉树,二叉树空缺的部分用0表示,如,表示的是以下树:
二叉树节点数据结构:
class TreeNode { //二叉树节点
public int value;
public TreeNode Lchild;
public TreeNode Rchild;
public TreeNode(int val) {
value = val;
Lchild = null;
Rchild = null;
}
}
二叉树数据结构:
class Tree{ //二叉树
public TreeNode root;
}
通过先序遍历数组(空子使用0表示)构建二叉树:
static int index = -1;
public static TreeNode initTree(int[] array){ //array为先序遍历数组,空子为0
TreeNode root = null;
index++;
if(index == array.length)return null;
if(0 == array[index])return null;
else{
root = new TreeNode(array[index]);
root.Lchild = initTree(array);
root.Rchild = initTree(array);
}
return root;
}
//创建二叉树
public static TreeNode createTree(int[] array){
index = -1; //初始化之前,需要将索引置为-1
return initTree(array);
}
5.2 层次遍历
使用辅助队列:
public static void levelTraverse(TreeNode root){
Queue<TreeNode> queue = new LinkedList<>();
queue.offer(root);
while(!queue.isEmpty()){
TreeNode q = queue.poll();
System.out.print(q.value+" ");
if(q.Lchild!=null){
queue.offer(q.Lchild);
}
if(q.Rchild!=null){
queue.offer(q.Rchild);
}
}
}
遍历结果:1 2 8 3 7 9 4 10 5 6
5.3 先序遍历
递归版本:
public static void pre_Traverse_re(TreeNode root) {
if(root!=null){
System.out.print(root.value+" ");
pre_Traverse_re(root.Lchild);
pre_Traverse_re(root.Rchild);
}
}
使用辅助栈的非递归A版本:
public static void pre_Traverse_1(TreeNode root){
Stack<TreeNode> stack = new Stack<>();
stack.push(root);
while (!stack.isEmpty()){
TreeNode q = stack.pop();
System.out.print(q.value+" ");
if(q.Rchild!=null){
stack.push(q.Rchild);
}
if(q.Lchild!=null){
stack.push(q.Lchild);
}
}
}
使用辅助栈的非递归B版本:
public static void pre_Traverse_2(TreeNode root){
Stack<TreeNode> stack = new Stack<>();
TreeNode q = root; //用q来遍历节点
while (q!=null || !stack.isEmpty()){
if(q!=null){
System.out.print(q.value+" ");
stack.push(q);
q = q.Lchild;
}else { //如果指向了null,则重新指向从栈中退出的结点
q = stack.pop();
q = q.Rchild;
}
}
}
遍历结果:1 8 9 10 2 7 3 4 6 5
5.4 中序遍历
递归版本:
public static void in_Traverse_re(TreeNode root){
if(root!=null){
in_Traverse_re(root.Lchild);
System.out.print(root.value+" ");
in_Traverse_re(root.Rchild);
}
}
使用辅助栈的非递归版本:
public static void in_Traverse(TreeNode root){
Stack<TreeNode> stack = new Stack<>();
TreeNode q = root;
while (q!=null || !stack.isEmpty()){
if(q!=null){
stack.push(q);
q = q.Lchild;
}else {
q = stack.pop();
System.out.print(q.value+" ");
q = q.Rchild;
}
}
}
5.5 后序遍历
递归版本:
public static void pos_Traverse_re(TreeNode root){
if(root!=null){
pos_Traverse_re(root.Lchild);
pos_Traverse_re(root.Rchild);
System.out.print(root.value+" ");
}
}
利用两个栈的非递归版本:
public static void pos_Traverse(TreeNode root){
Stack<TreeNode> stack1 = new Stack<>();
Stack<TreeNode> stack2 = new Stack<>();
TreeNode q = root;
//按照根右左的顺序遍历树
while (q!=null || !stack1.isEmpty()){
if(q!=null){
stack2.push(q);
stack1.push(q);
q = q.Rchild;
}else {
q = stack1.pop();
q = q.Lchild;
}
}
//将方向倒过来就是左右根,即为后序遍历结果
while (!stack2.isEmpty()){
q = stack2.pop();
System.out.print(q.value+" ");
}
}
使用一个双向队列保存结果:
public List<Integer> pos_Traverse(TreeNode root) {
Deque<TreeNode> stack = new LinkedList<>();
LinkedList<Integer> result = new LinkedList<>();
if(root == null) return result;
TreeNode p = root;
while (p!=null || !stack.isEmpty()){
if(p!=null){
result.offerFirst(p.val);
stack.push(p);
p = p.right;
}else {
p = stack.pop();
p = p.left;
}
}
return result;
}
6、二叉树相关问题
6.1 二叉树的深度
定义:二叉树中左右子树中最大深度+1
public static int depth(TreeNode root){
if(root == null)return 0;
else {
return Math.max(depth(root.Lchild),depth(root.Rchild))+1;
}
}
6.2 判断是否是平衡二叉树
定义:平衡二叉树每个左右子树最大深度差不大于1
public static boolean isBalence(TreeNode root){
if(root == null)return true;
else {
int left = depth(root.Lchild);
int riht = depth(root.Rchild);
return Math.abs(left-riht)<2;
}
}
public static boolean isBalenceTree(TreeNode root){
if(root!=null){
return isBalence(root)&&isBalenceTree(root.Lchild)&&isBalenceTree(root.Rchild);
}
return true;
}
6.3 交换左右子树
public static void exchange(TreeNode root){//交换左右子
if(root!=null){
TreeNode tem = root.Lchild;
root.Lchild = root.Rchild;
root.Rchild = tem;
exchange(root.Lchild);
exchange(root.Rchild);
}
}
6.4 判断是否为子树
public static boolean hasSubTree(TreeNode root1,TreeNode root2){
if(root2 == null)return true; //先判断root2是否先为空
if(root1 == null)return false;//root2不为空,root1为空则返回false
if(root1.value != root2.value)return false;
//root1 root2同时向左和向右查找
return hasSubTree(root1.Lchild, root2.Lchild)&&hasSubTree(root1.Rchild, root2.Rchild);
}
public static boolean isSubTree(TreeNode root1,TreeNode root2){
if(root2 == null)return true;
if(root1 == null)return false;
//从根左右的顺序查找,如果有一个子树就返回为真
return hasSubTree(root1,root2)||
isSubTree(root1.Lchild,root2)||
isSubTree(root1.Rchild,root2);
}
6.5 判断是否为同一棵树
public static boolean isSame(TreeNode root1,TreeNode root2){
if(root1!=null && root2!=null){
if(root1.value == root2.value){
return isSame(root1.Lchild,root2.Lchild)&&isSame(root1.Rchild,root2.Rchild);
}else {
return false;
}
}
if(root1==null && root2==null) return true;
return false;
}
7、哈夫曼树和哈夫曼编码
7.1 概念
哈夫曼树:给定N个权值作为N个叶子结点,构造一棵二叉树,若该树的带权路径长度达到最小,称这样的二叉树为最优二叉树,也称为哈夫曼树(Huffman Tree)。哈夫曼树是带权路径长度最短的树,权值较大的结点离根较近。
哈夫曼编码:是一种编码方式,完全依据字符出现概率来构造异字头的平均长度最短的码字,有时称之为最佳编码。
参考文章哈夫曼树的特点:
- 满二叉树不一定是哈夫曼树
- 哈夫曼树中权越大的叶子离根越近 (很好理解,WPL最小的二叉树)
- 具有相同带权结点的哈夫曼树不惟一
- 哈夫曼树的结点的度数为 0 或 2, 没有度为 1 的结点。
- 包含 n 个叶子结点的哈夫曼树中共有 2n – 1 个结点。
- 包含 n 棵树的森林要经过 n–1 次合并才能形成哈夫曼树,共产生 n–1 个新结点
7.2 哈夫曼树的构造方法:
算法流程:
- 给定一个元素集合array
- 分别找到最小的两个元素a,b
- 新建一个值为c(c=a+b)的二叉树结点,左右子分别指向值为a,b的结点
- 集合array删除a,b元素
- 如果array不为空,新增c元素
- 重复步骤2,直到元素集合array为空
代码:
//从树结点列表中找到值最小的一个节点
private static TreeNode min(ArrayList<TreeNode> arrayList){
int Min = arrayList.get(0).value; //最小节点的值
TreeNode MinNode = arrayList.get(0);//最小节点
for (int i=1;i<arrayList.size();i++){
if(arrayList.get(i).value<Min){
Min = arrayList.get(i).value;
MinNode = arrayList.get(i);
}
}
return MinNode;
}
public static TreeNode creatHuffmanTree(ArrayList<TreeNode> arrayList){
TreeNode root = null;
while (!arrayList.isEmpty()){
TreeNode a = min(arrayList);
arrayList.remove(a); //移除最小节点
TreeNode b = min(arrayList);
arrayList.remove(b); //移除之后的最小节点
int c = a.value+b.value;
root = new TreeNode(c); //新建节点
if(!arrayList.isEmpty())arrayList.add(root);
root.Lchild = a; //指定左子
root.Rchild = b; //指定右子
}
return root;
}
图示:
例:有4 个结点 a, b, c, d,权值分别为 7, 5, 2, 4,构造哈夫曼树。
1.初始权值集合:
2.在F中选取两棵根结点权值最小的树作为左右子树构造一棵新的二叉树,且置新的二叉树的根结点的权值为左右子树根结点的权值之和。在F中删除这两棵树,同时将新的二叉树加入F中.
3.重复,直到F只含有一棵树为止.(得到哈夫曼树)
7.3 哈夫曼编码
哈夫曼编码:
- 以电文中的字符作为叶子结点构造二叉树。
- 将二叉树中结点引向其左孩子的分支标 ‘0’,引向其右孩子的分支标 ‘1’。
- 每个字符的编码即为从根到每个叶子的路径上得到的 0, 1 序列。如此得到的即为二进制前缀编码。
代码:
private static Stack<Integer> stack = new Stack<>();
public static void HuffmanCode(TreeNode root){
if(root!=null){
if(root.Lchild!=null){
stack.push(0);
HuffmanCode(root.Lchild);
}
if(root.Rchild!=null){
stack.push(1);
HuffmanCode(root.Rchild);
}
if(root.Lchild == null && root.Rchild == null){//如果该结点是叶子节点
for(int i=0;i<stack.size();i++){
System.out.print(stack.get(i)); //打印出栈中的所有元素
}
stack.pop(); //退出栈顶元素,回溯
System.out.println();
}
if(root.Lchild != null && root.Rchild != null){ //当左右节点都遍历完时
if(!stack.isEmpty()){
stack.pop(); //当前节点也要退出,回溯到上一个节点
}
}
}
}
8、二叉树功能测试代码
测试代码:
public static void main(String[]args){
int[] array = {1,2,3,0,4,5,0,0,6,0,0,7,0,0,8,0,9,10,0,0,0};
int[] array2 ={1,2,3,0,4,5,0,0,6,0,0,7,0,0,8,0,9,0,0};
// 测试createTree(),exchange(),isSame(),depth(),isBalenceTree()函数
Tree tree1 = new Tree();
Tree tree2 = new Tree();
tree1.root = Tree.createTree(array);
tree2.root = Tree.createTree(array2);
System.out.println("树1树2是否一样:"+Tree.isSame(tree1.root,tree2.root));
System.out.println("树2是否是树1的子树:"+Tree.isSubTree(tree1.root,tree1.root));
System.out.println("交换左右子前(先序遍历)");
Tree.pre_Traverse_1(tree1.root);
System.out.println();
Tree.exchange(tree1.root);
System.out.println("交换左右子后(先序遍历)");
Tree.pre_Traverse_1(tree1.root);
System.out.println();
System.out.println("深度:"+Tree.depth(tree1.root));
System.out.println("是否是平衡二叉树:"+Tree.isBalenceTree(tree1.root));
// 测试哈夫曼编码
int[] haffman = {7,19,2,6,32,3,21,10};
ArrayList<TreeNode> arrayList =
new ArrayList<TreeNode>();
for(int e:haffman){
arrayList.add(new TreeNode(e));
}
Tree tree = new Tree();
tree.root = Tree.creatHuffmanTree(arrayList);
System.out.println("哈夫曼编码:");
Tree.HuffmanCode(tree.root);
}
五、并查集
1、概念
并查集(Union Find) 是一种用于管理分组的数据结构。它具备两个操作:(1)查询元素a和元素b是否为同一组 (2) 将元素a和b合并为同一组。
并查集的结构:
使用树形结构来表示,则每一组都对应一棵树,两个元素的根一致则属于同一棵树。查找时,只要找到根节点就可知属于哪个集合;合并时只需要将一组的根与另一组的根相连即可。
2、并查集的实现
class UnionFind{
public int[] pre; //存储每个结点的前驱结点,即集合的代表元
public int[] rank; //记录树的高度
public UnionFind(int N){//N表示最大存储元素
pre = new int[N];
rank = new int[N];
for(int i=0;i<N;i++){
pre[i] = i; //初始化前驱节点,每个结点的上级都是自己
rank[i] = 1; //每个结点构成的树的高度为 1
}
}
public int find0(int x){
if(pre[x] == x) return x; //递归出口:如果x的上级为x本身,则 x为根结点
return find0(pre[x]);
}
public int find(int x) //改进查找算法:完成路径压缩,将 x的上级直接变为根结点,那么树的高度就会大大降低
{
if(pre[x] == x) return x; //递归出口:x的上级为 x本身,即 x为根结点
return pre[x] = find(pre[x]); //此代码相当于先找到根结点 rootx,然后 pre[x]=rootx
}
public boolean isSame(int x, int y) //判断两个结点是否连通
{
return find(x) == find(y); //判断两个结点的根结点(即代表元)是否相同
}
public boolean join(int x,int y)
{
x = find(x); //寻找 x的代表元
y = find(y); //寻找 y的代表元
if(x == y) return false; //如果 x和 y的代表元一致,说明他们共属同一集合,则不需要合并,返回 false,表示合并失败;否则,执行下面的逻辑
if(rank[x] > rank[y]) pre[y]=x; //如果 x的高度大于 y,则令 y的上级为 x
else //否则
{
if(rank[x]==rank[y]) rank[y]++; //如果 x的高度和 y的高度相同,则令 y的高度加1
pre[x]=y; //让 x的上级为 y
}
return true; //返回 true,表示合并成功
}
}
3、总结
- 用来代表某集合的元素称为此集合的代表元。
- 一个集合内的所有元素组织成以代表元为根的树形结构。
- 对于每一个元素 x,pre[x] 存放 x 在树形结构中的前驱节点(如果 x 是根节点,则令pre[x] = x)。
- 对于查找操作,可以沿着pre[x]不断在树形结构中向上移动,直到查找到代表元。
- 对于合并操作,只需将一个树型结构的代表元作为另一个树型结构的子结构。
六、图
1、概念
图的定义:由顶点的有穷非空集合和顶点之间边的集合组成。记为:
其中, 表示一个图,是图中的顶点的集合,是图 中边的集合。
图的特点:是一种较线性表和树更加复杂的数据结构,在图形结构中,结点之间的关系可以是任意的,图中任意两个数据元素之间都可能相关。
2、图的分类
2.1 无向图
到之间的边没有方向,则称这条边为无向边,用无序偶对来表示。
如果图中任意两个顶点之间的边都是无向边,则称该图为无向图,无向图顶点的边数叫做度。
无向图示例:
2.2 有向图
到的边有方向,则称这条边为有向边,也称为弧。用有序偶来表示,称为弧尾(Tail),称为弧头(Head)。
如果图中任意两个顶点之间的边都是有向边,则称该图为有向图(Directed graphs)。有向图顶点分为入度(箭头朝自己)和 出度(箭头朝外)。
有向图示例:
2.3 简单图
定义:在图中,若不存在顶点到其自身的边,且同一条边不重复出现,则称这样的图为简单图。
如下所示的两个图就不属于简单图:
2.4 完全无向图
在无向图中,如果任意两个顶点之间都存在边,则称该图为无向完全图
如下图所示即为一个无向完全图:
2.5 完全有向图
在有向图中,如果任意两个顶点之间都存在方向互为相反的两条弧,则称该图为有向完全图。
如下图所示即为一个有向完全图:
3、图的存储结构
3.1 邻接矩阵表示法
设图有n个顶点,则图表示为A.Edge[n][n]
第行表示:以为入度
第列表示:以为出度
以邻接矩阵表示为:
注:0表示两个顶点之间没有弧,1表示两个顶点之间相连,权值为1
优点: 容易实现各种图的操作
缺点: 空间效率低
数据结构代码:
public abstract class Graph {
public int[][] edge;
public char[] vertext;
public Graph(char[] chars){
int n = chars.length;
edge = new int[n][n];
vertext = new char[n];
for (int i=0;i<n;i++){
for(int j=0;j<n;j++){
edge[i][j] = 0;
}
}
vertext = chars;
}
public void addArc(int i,int j,int value){
edge[i][j] = value;
}
public abstract void DFS_traverse_re(); //深度优先搜索(递归版)
public abstract void DFS_traverse();//深度优先搜索(非递归版)
public abstract void BFS_traverse_re(); //广度优先搜索(递归版)
public abstract void BFS_traverse();//广度优先搜索(非递归版)
}
3.2 邻接表表示法
数据结构表示:
优点: 空间效率高,容易找到顶点的邻接点
缺点: 判断顶点之间是否有边等操作时不方便
数据结构代码:
//邻接表数据结构
class abstract Graph2 {
public Head[] heads;
public Graph2(char[] chars){
heads = new Head[chars.length];
for(int i=0;i<chars.length;i++){
heads[i]=new Head(chars[i]);
}
}
public void addArc(int i,char ch,int info){
Vertex vertex = new Vertex(ch,info);
Vertex p = null;
if(heads[i].firstNode==null){//如果是第一个节点,则直接插入该顶点后面
heads[i].firstNode = vertex;
return;
}else { //否则使用尾插法进行插入
p = heads[i].firstNode;
}
while (p.next!=null){
p = p.next;
}
p.next = vertex;
}
public abstract void DFS_traverse();//深度优先搜索
public abstract void BFS_traverse();//广度优先搜索
}
//链表元素数据结构
class Vertex{
public char vertex;
public int info;
public Vertex next;
public Vertex(char ch,int info){
vertex = ch;
this.info = info;
next = null;
}
}
//表头元素数据结构
class Head{
public char vertex;
public Vertex firstNode;
public Head(char ch){
vertex = ch;
firstNode = null;
}
}
4、图的遍历
4.1 深度优先遍历(Depth First Search)
遍历过程:
1. 首先以一个未被访问过的顶点作为起始顶点,沿当前顶点的边走到未访问过的顶点
2. 当没有未访问过的顶点时则回到上一个顶点,继续试探别的顶点,直至所有的顶点都被访问过
图示:
遍历结果:A、B、E、F、D、C
4.11 邻接矩阵深度优先遍历过程(代码)
深度优先遍历(递归版):
public void DFS_traverse_re(){ //递归版深度优先搜索
//有可能有独立的顶点,所以需要将所有顶点遍历
for(int i=0;i<vertex.length;i++){ //按顺序从每个顶点开始遍历
if(visited[i]==0){
DFS_re(i);
}
}
}
public void DFS_re(int i){
visit(vertex[i]); //访问该结点
visited[i]=1; //访问标志位置1
for(int j=0;j<vertex.length;j++){
if(edge[i][j]!=0 && visited[j]==0){ //联通的点且未被访问过
DFS_re(j);
}
}
}
深度优先遍历(非递归版):
public void DFS_traverse(){ //非递归版深度优先搜索
clearVisitState();
for(int i=0;i<vertex.length;i++){
if(visited[i]==0){ //未被访问过
DFS(i);
}
}
}
public void DFS(int i){
Stack<Integer> stack = new Stack<>();
stack.push(i);
while (!stack.isEmpty()){
int tem = stack.pop();
if(visited[tem]==0){ //出栈如果没被访问过则访问
visit(vertex[tem]);
visited[tem]=1;
}
for(int j=vertex.length-1;j>0;j--){ //从后往前找
if(edge[tem][j]!=0 && visited[j]==0){ //如果和出栈的节点相连则进栈
stack.push(j);
}
}
}
}
4.12 邻接表深度优先遍历过程(代码)
深度优先遍历(递归版)
public void DFS_traverse_re(){
for(int i=0;i<heads.length;i++){
if(visited[i]==0){ //未被访问过
DFS_re(i);
}
}
}
public void DFS_re(int i){
visit(heads[i].vertex);
visited[i]=1;
Vertex p = heads[i].firstNode;
while (p!=null){ //遍历i节点相邻的节点
int index = find(p.vertex); //根据字符找到索引
if(visited[index]==0){
DFS_re(index);
}
p = p.next;
}
}
//辅助函数
public void visit(char vertex){ //访问图结点
System.out.print(vertex+" ");
}
public int find(char ch){
int index = -1;
for(int i=0;i<heads.length;i++){
if(ch == heads[i].vertex){
index = i;
break;
}
}
return index;
}
深度优先遍历(非递归版):
说明:该版本需要在建立表头链表时使用头插法,否则在下方【加标】处无法逆方向从链表中将元素入栈,从而导致遍历的结果顺序可能不一样
public void DFS_traverse(){
for(int i=0;i<heads.length;i++){
if(visited[i]==0){ //未被访问过
DFS(i);
}
}
}
public void DFS(int i){
Stack<Integer> stack = new Stack<>();
stack.push(i);
while (!stack.isEmpty()){
int tem = stack.pop();
if(visited[tem]==0){//出栈元素如果没有访问过则访问
visit(heads[tem].vertex);
visited[tem]=1; //访问后再标记
}
Vertex p = heads[tem].firstNode;
while (p!=null){ //【加标】遍历出栈元素的邻接节点
int index = find(p.vertex);
if(visited[index]==0){
stack.push(index);
}
p=p.next;
}
}
}
4.2 广度优先搜索(Depth First Search)
4.21 邻接矩阵广度优先遍历过程(代码)
广度优先遍历:
public void BFS_traverse(){ //广度优先搜索
for(int i=0;i<vertex.length;i++){
if(visited[i]==0){ //未被访问过
BFS(i);
}
}
}
public void BFS(int i){
Queue<Integer> queue = new LinkedList<>();
queue.offer(i);
visited[i]=1;//入队后访问标志位置1
while (!queue.isEmpty()){
int tem = queue.poll();
visit(vertex[tem]);
for(int j=0;j<vertex.length;j++){
if(edge[tem][j]!=0 && visited[j]==0){ //将和刚访问的结点相连的点入队列
queue.offer(j);
visited[j]=1;
}
}
}
}
4.22 邻接表广度优先遍历过程(代码)
广度优先遍历:
public void BFS_traverse(){
for(int i=0;i<heads.length;i++){
if(visited[i]==0){ //未被访问过
BFS(i);
}
}
}
public void BFS(int i){
Queue<Integer> queue = new LinkedList<>();
queue.offer(i);
visited[i]=1;
while (!queue.isEmpty()){
int tem = queue.poll();
visit(heads[tem].vertex);
Vertex p = heads[tem].firstNode;
while (p!=null){
if(visited[find(p.vertex)]==0){//未访问过
queue.offer(find(p.vertex));
visited[find(p.vertex)]=1;
}
p = p.next;
}
}
}
public void visit(char vertex){ //访问图结点
System.out.print(vertex+" ");
}
public int find(char ch){
int index = -1;
for(int i=0;i<heads.length;i++){
if(ch == heads[i].vertex){
index = i;
break;
}
}
return index;
}
4.3 主代码及测试
package Graph;
import java.util.LinkedList;
import java.util.Queue;
import java.util.Stack;
public class Graph{
public static void main(String[] args){
char[] chars = {'A','B','C','D','E','F'};
// 邻接表表示
Graph2 graph2 = new Graph2(chars);
graph2.addArc(0,'B',1); //A-B
graph2.addArc(0,'C',1); //A-C
graph2.addArc(0,'D',1); //A-D
graph2.addArc(1,'A',1); //B-A
graph2.addArc(1,'E',1); //B-E
graph2.addArc(1,'F',1); //B-F
graph2.addArc(2,'A',1); //C-A
graph2.addArc(3,'A',1); //D-A
graph2.addArc(3,'F',1); //D-F
graph2.addArc(4,'B',1); //E-B
graph2.addArc(5,'B',1); //F-B
graph2.addArc(5,'D',1); //F-D
for(int i=0;i<chars.length;i++){
System.out.print(graph2.heads[i].vertex+" ");
Vertex p = graph2.heads[i].firstNode;
while (p!=null){
System.out.print(p.vertex+":"+p.info+" ");
p = p.next;
}
System.out.println();
}
System.out.println("深度优先遍历(递归):");
graph2.DFS_traverse_re();
System.out.println();
System.out.println("深度优先遍历:");
graph2.DFS_traverse();
System.out.println();
System.out.println("广度优先遍历:");
graph2.BFS_traverse();
System.out.println();
// 邻接矩阵表示
Graph1 graph1 = new Graph1(chars);
graph1.addArc(0,1,1); //A-B
graph1.addArc(1,0,1);
graph1.addArc(1,4,1); //B-E
graph1.addArc(4,1,1);
graph1.addArc(1,5,1); //B-F
graph1.addArc(5,1,1);
graph1.addArc(5,3,1); //F-D
graph1.addArc(3,5,1);
graph1.addArc(0,3,1); //A-D
graph1.addArc(3,0,1);
graph1.addArc(0,2,1); //A-C
graph1.addArc(2,0,1);
for(int i=0;i<graph1.vertex.length;i++){
for(int j=0;j<graph1.vertex.length;j++){
System.out.print(graph1.edge[i][j]+" ");
}
System.out.println();
}
System.out.println("深度优先遍历(递归):");
graph1.DFS_traverse_re();
System.out.println();
System.out.println("深度优先遍历(非递归):");
graph1.DFS_traverse();
System.out.println();
System.out.println("广度优先遍历:");
graph1.BFS_traverse();
}
}
class Graph1 {
public int[][] edge; //邻接矩阵
public char[] vertex; //存放顶点符号
public int[] visited; //标记访问点
public Graph1(char[] chars){ //通过字符数组初始化图
int n = chars.length;
edge = new int[n][n];
vertex = new char[n];
visited = new int[n];
for (int i=0;i<n;i++){
for(int j=0;j<n;j++){
edge[i][j] = 0;
}
visited[i]=0;
}
vertex = chars;
}
public void addArc(int i,int j,int value){ //添加弧
edge[i][j] = value;
}
public void DFS_traverse_re(){ //递归版深度优先搜索
clearVisitState();
for(int i=0;i<vertex.length;i++){
if(visited[i]==0){
DFS_re(i);
}
}
}
public void DFS_re(int i){
visit(vertex[i]); //访问该结点
visited[i]=1; //访问标志位置1
for(int j=0;j<vertex.length;j++){
if(edge[i][j]!=0 && visited[j]==0){ //联通的点且未被访问过
DFS_re(j);
}
}
}
public void DFS_traverse(){ //非递归版深度优先搜索
clearVisitState();
for(int i=0;i<vertex.length;i++){
if(visited[i]==0){ //未被访问过
DFS(i);
}
}
}
public void DFS(int i){
Stack<Integer> stack = new Stack<>();
stack.push(i);
while (!stack.isEmpty()){
int tem = stack.pop();
if(visited[tem]==0){ //出栈如果没被访问过则访问
visit(vertex[tem]);
visited[tem]=1;
}
for(int j=vertex.length-1;j>0;j--){ //从后往前找
if(edge[tem][j]!=0 && visited[j]==0){ //如果和出栈的节点相连则进栈
stack.push(j);
}
}
}
}
public void BFS_traverse(){ //广度优先搜索
clearVisitState();
for(int i=0;i<vertex.length;i++){
if(visited[i]==0){ //未被访问过
BFS(i);
}
}
}
public void BFS(int i){
Queue<Integer> queue = new LinkedList<>();
queue.offer(i);
visited[i]=1;
while (!queue.isEmpty()){
int tem = queue.poll();
visit(vertex[tem]);
for(int j=0;j<vertex.length;j++){
if(edge[tem][j]!=0 && visited[j]==0){ //将和刚访问的结点相连的点入队列
queue.offer(j);
visited[j]=1;
}
}
}
}
public void clearVisitState(){
//每次遍历前将所有的结点都置为未访问状态
//否则进行多次测试会出现不能遍历的情况
for(int i=0;i<vertex.length;i++){
visited[i]=0;
}
}
public void visit(char vertex){ //访问图结点
System.out.print(vertex+" ");
}
}
class Graph2 {
public Head[] heads; //头表
public int[] visited; //存放访问状态
public Graph2(char[] chars){ //通过字符数组创建邻接表
heads = new Head[chars.length];
visited = new int[chars.length];
for(int i=0;i<chars.length;i++){
heads[i]=new Head(chars[i]);
visited[i]=0;
}
}
public void addArc(int i,char ch,int info){
Vertex vertex = new Vertex(ch,info);
Vertex p = null;
if(heads[i].firstNode==null){//如果是第一个节点,则直接插入该顶点后面
heads[i].firstNode = vertex;
return;
}else { //否则使用尾插法进行插入
p = heads[i].firstNode;
}
while (p.next!=null){
p = p.next;
}
p.next = vertex;
}
public void DFS_traverse_re(){
clearVisitState();
for(int i=0;i<heads.length;i++){
if(visited[i]==0){ //未被访问过
DFS_re(i);
}
}
}
public void DFS_re(int i){
visit(heads[i].vertex);
visited[i]=1;
Vertex p = heads[i].firstNode;
while (p!=null){ //遍历i节点相邻的节点
int index = find(p.vertex); //根据字符找到索引
if(visited[index]==0){
DFS_re(index);
}
p = p.next;
}
}
public void DFS_traverse(){
clearVisitState();
for(int i=0;i<heads.length;i++){
if(visited[i]==0){ //未被访问过
DFS(i);
}
}
}
public void DFS(int i){
Stack<Integer> stack = new Stack<>();
stack.push(i);
while (!stack.isEmpty()){
int tem = stack.pop();
if(visited[tem]==0){
visit(heads[tem].vertex); //访问出栈的元素
visited[tem]=1;
}
Vertex p = heads[tem].firstNode;
while (p!=null){ //【加标】遍历出栈元素的邻接节点
int index = find(p.vertex);
if(visited[index]==0){
stack.push(index);
}
p=p.next;
}
}
}
public void BFS_traverse(){
clearVisitState();
for(int i=0;i<heads.length;i++){
if(visited[i]==0){ //未被访问过
BFS(i);
}
}
}
public void BFS(int i){
Queue<Integer> queue = new LinkedList<>();
queue.offer(i);
visited[i]=1;
while (!queue.isEmpty()){
int tem = queue.poll();
visit(heads[tem].vertex);
Vertex p = heads[tem].firstNode;
while (p!=null){
if(visited[find(p.vertex)]==0){//未访问过
queue.offer(find(p.vertex));
visited[find(p.vertex)]=1;
}
p = p.next;
}
}
}
public void visit(char vertex){ //访问图结点
System.out.print(vertex+" ");
}
public int find(char ch){
int index = -1;
for(int i=0;i<heads.length;i++){
if(ch == heads[i].vertex){
index = i;
break;
}
}
return index;
}
public void clearVisitState(){
//每次遍历前将所有的结点都置为未访问状态
//否则进行多次测试会出现不能遍历的情况
for(int i=0;i<heads.length;i++){
visited[i]=0;
}
}
}
class Vertex{ //顶点数据结构
public char vertex; //存放顶点字符
public int info; //存放权值
public Vertex next; //下一个结点引用
public Vertex(char ch,int info){
vertex = ch;
this.info = info;
next = null;
}
}
class Head{ //头表数据结构
public char vertex; //存放顶点字符
public Vertex firstNode; //指向第一个相邻结点
public Head(char ch){
vertex = ch;
firstNode = null;
}
}
5、图的应用——最小生成树
生成树: 无向图中相互连通,且个顶点只有条边的联通子图叫生成树。
5.1 kruskal算法
算法流程:
输入: 图G
输出: 图G的最小生成树边集Enew
具体流程:
(1)将图G看做一个森林,每个顶点为一棵独立的树
(2)将所有的边加入集合S,即一开始S = E
(3)从S中拿出一条最短的边(u,v),如果(u,v)不在同一棵树内,则连接u,v合并这两棵树,同时将(u,v)加入生成树的边集Enew
(4)重复(3)直到所有点属于同一棵树,边集Enew就是一棵最小生成树
图示:
代码:
//边结构
class Edge{
int start;
int end;
int value;
public Edge(int a,int b,int val){
start = a;
end = b;
value = val;
}
}
//并查集结构
class UnionFind{
public int[] pre; //存储每个结点的前驱结点,即集合的代表元
public int[] rank; //记录树的高度
public UnionFind(int N){//N表示最大存储元素
pre = new int[N];
rank = new int[N];
for(int i=0;i<N;i++){
pre[i] = i; //初始化前驱节点,每个结点的上级都是自己
rank[i] = 1; //每个结点构成的树的高度为 1
}
}
public int find(int x) //改进查找算法:完成路径压缩,将 x的上级直接变为根结点,那么树的高度就会大大降低
{
if(pre[x] == x) return x; //递归出口:x的上级为 x本身,即 x为根结点
return pre[x] = find(pre[x]); //此代码相当于先找到根结点 rootx,然后 pre[x]=rootx
}
public boolean isSame(int x, int y) //判断两个结点是否连通
{
return find(x) == find(y); //判断两个结点的根结点(即代表元)是否相同
}
public boolean join(int x,int y)
{
x = find(x); //寻找 x的代表元
y = find(y); //寻找 y的代表元
if(x == y) return false; //如果 x和 y的代表元一致,说明他们共属同一集合,则不需要合并,返回 false,表示合并失败;否则,执行下面的逻辑
if(rank[x] > rank[y]) pre[y]=x; //如果 x的高度大于 y,则令 y的上级为 x
else //否则
{
if(rank[x]==rank[y]) rank[y]++; //如果 x的高度和 y的高度相同,则令 y的高度加1
pre[x]=y; //让 x的上级为 y
}
return true; //返回 true,表示合并成功
}
}
//kruskal算法求最小生成树
public LinkedList<Edge> kruskal(){
LinkedList<Edge> edge_list = new LinkedList<Edge>();
LinkedList<Edge> new_edge_list = new LinkedList<Edge>();
UnionFind unionFind = new UnionFind(vertex.length);
for(int i=0;i<vertex.length;i++){
for(int j=0;j<vertex.length;j++){
if(edge[i][j]!=0){
Edge e = new Edge(i,j,edge[i][j]);
edge_list.add(e);
}
}
}
while (!edge_list.isEmpty()){
//找到权重最小的边
int min = edge_list.get(0).value;
int min_index = 0;
for(int i=1;i<edge_list.size();i++){
if(edge_list.get(i).value<min){
min = edge_list.get(i).value;
min_index = i;
}
}
Edge minEdge = edge_list.remove(min_index); //从边集合中删除最小边
//如果边的首尾顶点不是连通分量(不在一棵树上)
if(!unionFind.isSame(minEdge.start,minEdge.end)){
unionFind.join(minEdge.start,minEdge.end); //合并首尾顶点
new_edge_list.add(minEdge); //将该边加入生成树边集合中
}
}
return new_edge_list; //返回边结构链表
}
时间复杂度: 【查找最小边复杂度,查找是否属于同一个树,即是否连通复杂度】
适用: 适用于求稀疏图的最小生成树
5.2 prim算法
算法流程:
输入:一个加权连通图,其中顶点集合为V,边集合为E
输出:使用集合Vnew和Enew来描述所得到的最小生成树
算法流程:
(1)初始化:Vnew = {x},其中x为集合V中的任一节点(起始点),Enew = {};
(2)重复下列操作,直到所有节点加入Vnew:
<1>在集合V剩余元素中选取到集合Vnew权值最小的边(u, v),其中u为集合Vnew中的元素,而v则是V中没有加入Vnew的顶点(如果存在有多条满足前述条件即具有相同权值的边,则可任意选取其中之一);
<2>将v加入集合Vnew中,将(u, v)加入集合Enew中;
图示:
代码:
public LinkedList<Edge> prim(){
LinkedList<Edge> new_dege_list = new LinkedList<Edge>();
LinkedList<Integer> Vnew = new LinkedList<>();
LinkedList<Integer> V = new LinkedList<>(); //V是顶点集合,用索引存放
for(int i=0;i<vertex.length;i++){
V.add(i);
}
Vnew.add(V.remove(0)); //一开始将第一个节点放入Vnew集合
while (!V.isEmpty()){
// 从两个集合中分别取出一个点u,v,使得(u,v)边最短
int min = Integer.MAX_VALUE; //设定初始最小值
int min_i = 0; //Vnew索引
int min_j = 0; //V索引
for(int i=0;i<Vnew.size();i++){
for(int j=0;j<V.size();j++){
if(edge[Vnew.get(i)][V.get(j)]!=0 && edge[Vnew.get(i)][V.get(j)]<min){
min = edge[Vnew.get(i)][V.get(j)];
min_i = i;
min_j = j;
}
}
}
Integer u = Vnew.get(min_i);
Integer v = V.remove(min_j);
new_dege_list.add(new Edge(u,v,edge[u][v]));//最短的(u,v)边加入生成树边集合
Vnew.add(v); //使(u,v)最短的v点从V中移入Vnew;
}
return new_dege_list;
}
5.3 最小生成树算法测试
public static void main(String[] args){
char[] chars = {'A','B','C','D','E','F'};
// 邻接矩阵表示
Graph1 graph1 = new Graph1(chars);
graph1.addArc(0,1,6); //A-B
graph1.addArc(1,0,6);
graph1.addArc(0,2,1); //A-C
graph1.addArc(2,0,1);
graph1.addArc(0,3,5); //A-D
graph1.addArc(3,0,5);
graph1.addArc(1,4,3); //B-E
graph1.addArc(4,1,3);
graph1.addArc(1,2,5); //B-C
graph1.addArc(2,1,5);
graph1.addArc(2,3,5); //C-D
graph1.addArc(3,2,5);
graph1.addArc(2,4,6); //C-E
graph1.addArc(4,2,6);
graph1.addArc(2,5,4); //C-F
graph1.addArc(5,2,4);
graph1.addArc(3,5,2); //D-F
graph1.addArc(5,3,2);
graph1.addArc(4,5,6); //E-F
graph1.addArc(5,4,6);
for(int i=0;i<graph1.vertex.length;i++){
for(int j=0;j<graph1.vertex.length;j++){
System.out.print(graph1.edge[i][j]+" ");
}
System.out.println();
}
System.out.println("最小生成树:");
LinkedList<Edge> edges = new LinkedList<Edge>();
edges = graph1.prim();
// edges = graph1.kruskal();
for(Edge edge:edges){
System.out.println(graph1.vertex[edge.start]+"——"+edge.value+"——"+graph1.vertex[edge.end]);
}
}
输出结果:
最小生成树:
A——1——C
C——4——F
F——2——D
C——5——B
B——3——E
时间复杂度: 邻接矩阵表示 邻接表表示
6、图的应用——最短路径
6.1 迪杰斯特拉算法(Dijkstra)
适用范围: 非负权图
算法流程:
输入:一个带权联通图G
输出:dis表(存放指定点到其他各点的距离),pre表(存放各点的前驱节点)
算法流程:
(1)初始化dis表,表中存放A到其他各点的权值,不直接相邻则置为∞
(2)初始化pre表,表中存放各点的前驱节点,初始值均为A
(3)初始化一个空表path,现将A节点放入path中,重复执行下列操作,直到所有节点均放入path中:
<1>从dis表中找到未访问过的,且与A距离最短的节点N加入path表中
<2>循环遍历剩下的不在path表中的节点i:
if length(A,N)+length(N,i)<dis[i]{
更新dis表:dis[i]=length(A,N)+length(N,i)
更新pre表:pre[i]=N
}
图示:
代码:
int Max = Integer.MAX_VALUE; //定义无穷
public void Dijkstra(int i){ //从i节点开始
int[] visited = new int[vertex.length]; //存放节点访问标志
int[] pre = new int[vertex.length]; //存放节点前驱,只要向前查找遍历,就能找到i到该结点的路径
int[] dis = new int[vertex.length]; //存放i节点到该节点的最短距离
LinkedList<Integer> paths = new LinkedList<>(); //存放遍历过的节点
for(int k=0;k<vertex.length;k++){
visited[k] = 0; //初始化访问数组,0为未访问过
pre[k] = i; //初始化前驱节点,让每个节点的前驱为其自身
//初始化i到其他节点的距离
int value = edge[i][k];
//****注意:由于一开始定义邻接矩阵时规定0为两节点不相连
//****而在寻找最短路径时,两节点不相邻应为距离为无穷远
//****为保证代码一致性且后续不出错,将不相邻的点改为无穷远
if (i!=k && value == 0){
dis[k] = Max;
}else {
dis[k] = value;
}
}
//先访问i节点
paths.add(i);
visited[i] = 1;
while (paths.size()<vertex.length){ //将所有节点访问完退出循环
int min = Max; //定义最小值
int node = 0; //定义最小值的索引
//查找剩下未访问节点中到i距离最小的点
for(int k=0;k<dis.length;k++){
if(visited[k]!=1 && dis[k]<min){
min = dis[k];
node = k;
}
}
paths.add(node); //将距离最小的节点加入paths
visited[node]=1; //将该最小节点访问标志位置1
for(int k = 0;k<vertex.length;k++){ //用新加入的点更新未访问的dis表
if(visited[k]!=1 && edge[node][k]!=0 &&edge[i][node]!=0 && edge[node][k]+edge[i][node]<dis[k]){
dis[k] = edge[node][k]+edge[i][node];
pre[k] = node; //用更近的node点作为i到该结点的中间节点,即前驱节点
}
}
}
System.out.println("\ndis表:");
for (int e:dis){
System.out.print(e+" ");
}
System.out.println("\npre表:");
for (int e:pre){
System.out.print(e+" ");
}
System.out.println("\n轨迹:");
for(int k=0;k<pre.length;k++){
int m = k;
System.out.print(vertex[m]+":");
System.out.print(vertex[m]+" ");
while (m!=pre[m]){
m = pre[m];
System.out.print(vertex[m]+" ");
}
System.out.println();
}
}
输出结果:
dis表:
0 6 1 5 7 5
pre表:
0 0 0 0 2 2
轨迹:
A:A
B:B A
C:C A
D:D A
E:E C A
F:F C A
算法时间复杂度:对于单个点到其他各点是,如果要求图中所有点的最短距离,则时间复杂度为
6.2 弗洛伊德算法(Floyd)
使用范围: 可以是带负权的图,但不支持负权环
算法流程:
输入:一个带权联通图G
输出:dis二维矩阵(存放各点之间的最短距离),pre二维矩阵(存放各点路径的前驱节点)
算法流程:
(1)初始化dis矩阵,存放各个相邻结点之间的权值,不相邻的点置为∞
(2)初始化pre矩阵,都置为-1
(3)将各个结点分别作为中间节点N,执行如下操作:
遍历任意两个节点i,j的距离:
if length(i,N)+length(N,j)<dis[i][j]{
更新dis表:dis[i][j]=length(i,N)+length(N,j)
更新pre表:pre[i][j]=N
}
图示:
代码:
public void floyd(){
int[][] dis = new int[vertex.length][vertex.length]; //存放各点之间的最短距离
int[][] pre = new int[vertex.length][vertex.length]; //存放各点最短距离的前驱节点
for(int i=0;i<vertex.length;i++){
for(int j=0;j<vertex.length;j++){
if(i!=j && edge[i][j]==0){
//由于最初定义图节点用0表示不相邻,此处根据求最短路径现实意义,将不相邻距离改为无穷大
dis[i][j] = 1000;
}else {
dis[i][j] = edge[i][j];
}
pre[i][j] = -1;
}
}
for(int m=0;m<vertex.length;m++){ //m为中继节点
for(int i=0;i<vertex.length;i++){
for(int j=0;j<vertex.length;j++){
if(dis[i][m]+dis[m][j]<dis[i][j]){
dis[i][j]=dis[i][m]+dis[m][j]; //更新dis表
pre[i][j]=m; //更新pre表
}
}
}
}
System.out.println("各个结点的距离为:");
for(int i=0;i<vertex.length;i++){
for(int j=0;j<vertex.length;j++){
System.out.print(dis[i][j]+"\t");
}
System.out.println();
}
System.out.println("最短路径:");
for(int i=0;i<vertex.length;i++){
for(int j=0;j<vertex.length;j++){
System.out.print(vertex[i]+"到"+vertex[j]+":");
System.out.print(vertex[j]+"->");
findPath(pre,i,j);
System.out.println(vertex[i]);
}
}
}
//递归寻找前驱,寻找的过程就是最短路径
public void findPath(int[][]pre,int i,int j){
int m = pre[i][j];
if(m == -1)return;
System.out.print(vertex[m]+"->");
findPath(pre,i,m);
}
时间复杂度:
七、查找
1、查找表
概念: 查找表是一种称为集合的数据结构,是一种元素间的约束力最差的数据结构,元素间的关系仅为在同一各集合中。
查找表的操作: 查找、插入、删除
查找表的分类: 静态查找表、动态查找表
2、静态查找表
概念: 表的结构在初始化时就已经确定
查找算法: 顺序查找、折半查找、分块查找
2.1 顺序查找
图示:
核心代码:
public int search_seq(int key){
array[0] = key; //把关键字key存入首或者尾作为哨兵,加快查找速度
int i;
for(i = array.length-1;array[i]!=key;i--);
return i;
}
顺序查找的特点: 算法简单,对顺序表或者链表结构都适用,缺点是ASL太大,时间效率太低。
2.2 折半查找
核心代码:
//非递归版本
public int search_bin(int key){
int left = 0;
int right = array.length-1;
int mid;
while (left<=right){
mid = (left+right)/2;
if(array[mid]>key) right = mid-1;
else if(array[mid]<key) left = mid+1;
else if(array[mid] == key) return mid;
}
return -1;
}
//递归版本
public int search_bin_re(int key,int left,int right){
if(left>right) return -1;
int mid = left+right;
if(array[mid]>key) return search_bin_re(key,left,mid-1);
else if(array[mid]<key)return search_bin_re(key,mid+1,right);
else return mid;
}
特点 :查找的表必须是有序表,且存储结构必须是顺序存储
2.3 分块查找
思路: 先让数据分块有序,即分成若干个子表,要求每个子表中的元素都比后一块中的数值小(子表内未必有序)。然后将各子表中的最大关键字构成一个索引表,表中还要包含每个子表的起始地址。在块内进行顺序查找,块间进行折半查找。
特点: 块内无序,块间有序
查找: 块间折半查找,块内顺序查找
图示:
(是索引表查找的ASL,是块内查找的ASL)
3、 动态查找表
3.1 二叉排序树
二叉排序树: 又称二叉查找树(Binary Search Tree),亦称二叉搜索树,所有节点的值遵循 左<根<右 的规律。
二叉排序树的特点:
- 使用中序遍历会得到一组递增的序列
- 没有键值相等的节点
- 若左子树不空,则左子树上所有结点的值均小于它的根结点的值
- 若右子树不空,则右子树上所有结点的值均大于它的根结点的值
- 左右子树都是二叉排序树
构建二叉排序树代码:
public static BinSearchTree initTree(int[] array){
BinSearchTree root = new BinSearchTree(array[0]);
for(int i=1;i<array.length;i++){
insert(root,array[i]);
}
return root;
}
public static void insert(BinSearchTree root,int value){
if(root == null) return;
if(value<root.value){ //如果比根节点小,考虑插在左子中
if(root.LChild != null){ //如果左子不为空,在左子树中插入
insert(root.LChild,value);
}else { //如果左子为空,直接插在左子中
BinSearchTree child = new BinSearchTree(value);
root.LChild = child;
}
}else { //如果比根节点大,考虑插在右子中
if(root.RChild != null){ //如果右子不为空,插在右子树中
insert(root.RChild,value);
}else { //如果右子为空,直接插在右子中
BinSearchTree child = new BinSearchTree(value);
root.RChild = child;
}
}
}
二叉排序树删除节点:
【删除B节点】
设删除前中序遍历结果是 L P ··· S B R A
方法一:将B的左子树L接A的左子,B的右子树R接L的最右下方的节点S的右子
方法二:直接令S替换节点B
3.2 平衡二叉树
平衡二叉树(Balanced Binary Tree): 又被称为AVL树,是二叉搜索树改进的版本,在具有二叉搜索树性质的同时,且具有以下性质:
- 它是一 棵空树或它的左右两个子树的高度差的绝对值不超过1,
- 左右两个子树都是一棵平衡二叉树。
将二叉排序树调整为平衡二叉树:
八、排序
排序分类:
1、 插入排序
1.1 直接插入排序
排序思想: 在已形成的有序表中线性查找,并在适当位置插入,把原来位置上的元素向后顺移。
时间效率: 因为在最坏情况下,所有元素的比较次数总和为。最好情况是每次都不移动复杂度为 ,其他情况下也要考虑移动元素的次数。 故时间复杂度为
空间效率: 仅占用 1 个缓冲单元——
算法的稳定性: 稳定
代码实现:
public static void insertSort(int[] array){
int tem;
//i从0开始,防止只有一个元素时越界
for(int i=0; i<array.length; i++){//假定第一个记录有序
tem = array[i]; //先将待插入的元素放入临时位置
int j = i-1;
while (j>=0 && tem < array[j]){ //此处可以将array[0]作为哨兵,就可以少一个判断
array[j+1] = array[j];
j--;//只要子表元素比哨兵大就不断前移
}
array[j+1] = tem; //直到子表元素小于哨兵,将哨兵值送入
} //当前要插入的位置(包括插入到表首)
}
1.2 折半插入排序
排序思想: 既然子表有序且为顺序存储结构,则插入时采用折半查找定可加速。
优点: 比较次数大大减少,全部元素比较次数仅为 。
时间效率: 虽然比较次数大大减少,可惜移动次数并未减少, 所以排序效率仍为 。
空间效率: 仍为
稳 定 性: 稳定
补充: 若记录是链表结构,用直接插入排序无需移动元素,时间效率更高!但是单链表结构无法实现“折半查找”
1.3 2-路插入排序
排序思想: 前面的插入排序每次插入元素的时候都会移动较多的元素,二路插入排序对其进行了改善。思路是:以第一个元素作为比较元素,后面所有大于该元素的数全部放在前面,所有小于元素的数放在后面,大于或小于部分的元素在插入的时候使用直接插入排序来保证有序,当所有元素分配好后,其实数组已经变成两个有序区,再组合好就完成排序了。
1.3 表插入排序
排序思想: 顺序存储结构中,给每个记录增开一个指针分量,在排序过程中将指针内容逐
个修改为已经整理(排序)过的后继记录地址。
优点: 在排序过程中不移动元素,只修改指针。此方法具有链表排序和地址排序的特点
时间效率: 无需移动记录,只需修改指针值。但由于比较次数没有减少,故时间效率仍为 。
空间效率: 空间效率肯定低,因为增开了指针分量(但在运算过程中没有用到更多的辅助单元)
稳 定 性: 稳定
1.4 希尔(shell)排序
排序思想: 希尔排序是把记录按一定增量分组,对每组使用直接插入排序算法排序;随着增量逐渐减少,每组包含的关键词越来越多,当增量减至1时,整个记录恰被分成一组,算法便终止。
时间效率: ~ ——由经验公式得出
空间效率:
稳 定 性: 不稳定
图示:
代码:
public void shellSort(int[] array){
for(int gas=array.length/2;gas>0;gas=gas/2){
for(int k=0;k<gas;k++){
for(int i=k;i<array.length;i+=gas){ //对每组进行直接插入排序
int tem = array[i];
int j = i - gas;
while (j>=0 && tem < array[j]){
array[j+gas] = array[j];
j -= gas;
}
array[j+gas] = tem;
}
}
}
}
2、交换排序
交换排序的基本思想是: 两两比较待排序记录的关键码,如果发生逆序(即排列顺序与
排序后的次序正好相反),则交换之,直到所有记录都排好序为止。
2.1 冒泡排序
基本思路: 每趟不断将记录两两比较,并按“前小后大”(或“前大后小”)规则交换。
优点: 每趟结束时,不仅能挤出一个最大值到最后面位置,还能同时部分理顺其他元素;一
旦下趟没有交换发生,还可以提前结束排序。
前提: 顺序存储结构
![在这里插入图片描述]( x220)
时间效率: —因为要考虑最坏情况
空间效率: —只在交换时用到一个缓冲单元
稳定性: 稳定
代码:
public static void bubbleSort(int[] array){
int tem;
for(int i=0;i<array.length;i++){
boolean flag = true;//设定一个标记,若为true,则表示此次循环没有进行交换,也就是待排序列已经有序,排序已然完成。
//每次排出了最大的一项,后面的无需比较交换
for(int j=0;j<array.length-i-1;j++){
if(array[j+1]<array[j]){
tem = array[j+1];
array[j+1] = array[j];
array[j] = tem;
flag = false;
}
}
if(flag)break;
}
}
2.2 快速排序
基本思路: 从待排序列中任取一个元素 (例如取第一个) 作为中心,所有比它小的元素一律
前放,所有比它大的元素一律后放,形成左右两个子表;然后再对各子表重新选择中心元素
并依此规则调整,直到每个子表的元素只剩一个。此时便为有序序列了
前提: 顺序存储结构
时间效率: ——因为每趟确定的元素呈指数增加
空间效率: ——因为递归要用栈(存每层 low,high 和 pivot)
稳定性: 不稳定
代码:
public static int partion(int[] array,int low,int high){
int pivot = array[low]; //以第一个元素作为pivot
while (low<high){
while (low<high && array[high]>=pivot)high--; //从右往左找第一个小于pivot的元素array[high],注意不能是小于等于
array[low] = array[high];
while (low<high && array[low]<=pivot)low++;//从左往右找第一个大于pivot的元素array[low]
array[high] = array[low];
}
array[low] = pivot; //将空缺的位置放入pivot,完成了交换
return low; //此时low==high,即找到了以pivot分开成两部分的分界点
}
public static void quickSort(int[] array,int low,int high){
int position = partion(array,low,high); //找到了以pivot分开成两部分的分界点
if(position-1>low){ //对左边序列进行再分割
quickSort(array,low,position-1);
}
if(position+1<high){ //对右边序列进行再分割
quickSort(array,position+1,high);
}
}
3、选择排序
选择排序的基本思想是: 每一趟在后面 n-i 个待排记录中选取关键字最小的记录作为有序序列中的第 i 个记录。
3.1 简单选择排序
排序思想: 每经过一趟比较就找出一个最小值,与待排序列最前面的位置互换即可。
前提: 顺序存储结构
图示:
时间效率: ——移动次数少,但是比较次数多
空间效率: ——只需要一个临时空间
稳定性: 不稳定
代码:
public static void selectSort(int[] array){
int tem;
for(int i=0;i<array.length;i++){
int min = i;
for (int j=i;j<array.length;j++){
if(array[j]<array[min]){
min = j;
}
} //找到剩余序列中最小的值的下标
//将a[i]与最小值互换
tem = array[i];
array[i] = array[min];
array[min] = tem;
}
}
3.2 树形选择排序
基本思想: 与体育比赛时的淘汰赛类似,首先对 n 个记录的关键字进行两两比较,得到 n/2 个优胜者(关键字小者),作为第一步比较的结果保留下来。然后在这 n/2 个较小者之间再进行两两比较,…,如此重复,直到选出最小关键字的记录为止。
优点: 减少比较次数,加快排序速度
缺点: 空间效率低
时间效率:
空间效率:
3.3 堆排序
时间效率: 。因为整个排序过程中需要调用 n-1 次 heapAdjust( )算法,而算法本身耗时为;
空间效率: 。仅在第二个 for 循环中交换记录时用到一个临时变量。
稳定性: 不稳定。
优点: 对小文件效果不明显,但对大文件有效。
图示:
代码:
public static void heapSort(int[] array){
for(int i=array.length-1;i>0;i--){
heapAdjust(array,i); //按大根堆对数组进行调整,每次循环调整的范围从末尾-1
swap(array,0,i); //将第一个元素与末尾元素交换
}
}
public static void heapAdjust(int[] array,int end){ //end代表待调整的序列结尾索引
for(int i=(end+1)/2-1;i>=0;i--){ //i从第一个非叶子节点索引开始
int left = i*2+1; //指向左子节点
int big = left;
int right = (i+1)*2; //指向右子节点
if(right<=end && array[right]>array[left]){ //big指向较大的子节点
big = right;
}
if(array[i]<array[big]){ //如果父节点比big节点小,则交换位置
swap(array,i,big);
}
}
}
public static void swap(int[] array,int a,int b){ //交换数组中两个元素
int tem = array[a];
array[a] = array[b];
array[b] = tem;
}
4、 归并排序
基本思想: 采用经典的分治策略(分治法将问题分(divide)成一些小的问题然后递归求解,而治(conquer)的阶段则将分的阶段得到的各答案"修补"在一起,即分而治之)。
图示:
时间效率: ——按二叉树的方式分层
空间效率: ——每层都需要n个位置用来存放
稳定性: 稳定
public static void mergeSort(int[] array,int low,int high){
if(low<high){
int mid = (low+high)/2;
mergeSort(array, low, mid); //对左半部分进行排序
mergeSort(array, mid+1, high); //对右半部分进行排序
merge(array,low,mid,high); //将两组有序数组执行归并操作
}
}
public static void merge(int[] array,int low,int mid,int high) {
int[] tem = new int[high-low+1];
int i = low,j=mid+1;
int k=0;
//将左右两边指针分别向前移动,将较小的元素放进tem空间中
for(;i<=mid && j<=high;k++){
if(array[i]>array[j]){ //如果右边指针所指元素较小
tem[k] = array[j]; //将该元素放入tem空间
j++; //继续移动该方向指针
}else { //否则在左边进行相同的操作
tem[k] = array[i];
i++;
}
}
//如果一边指针没有指到尽头,就将该方向剩下(有序)元素依次放入tem空间中
while(j<=high)tem[k++]=array[j++];
while(i<=mid)tem[k++] = array[i++];
//将临时空间中的元素放回原数组中
for(int k2=0;k2<tem.length;k2++){
array[k2+low] = tem[k2];
}
}
5、 基数排序
排序思想: 是将整数按位数切割成不同的数字,然后按每个位数分别比较。由于整数也可以表达字符串(比如名字或日期)和特定格式的浮点数,所以基数排序也不是只能使用于整数。
☝参考文章:https://www.runoob.com/w3cnote/radix-sort.html图示:
时间效率: ——其中,d 为位数,r 为基数,n 为原数组个数。
空间效率:
稳定性: 稳定。
代码:
// 获取最大值的位数,用来确定桶的最大容量
public static int getMaxDigit(int[] arr) {
int maxValue = getMaxValue(arr);
return getNumLenght(maxValue);
}
//获取数组中的最大值
public static int getMaxValue(int[] arr) {
int maxValue = arr[0];
for (int value : arr) {
if (maxValue < value) {
maxValue = value;
}
}
return maxValue;
}
//获取值的位数
public static int getNumLenght(long num) {
if (num == 0) {
return 1;
}
int lenght = 0;
for (long temp = num; temp != 0; temp /= 10) {
lenght++;
}
return lenght;
}
//用于数组扩充容量
public static int[] arrayAppend(int[] arr, int value) {
arr = Arrays.copyOf(arr, arr.length + 1);
arr[arr.length - 1] = value;
return arr;
}
//基数排序
public static void radixSort(int[] arr) {
int mod = 10;
int dev = 1;
int maxDigit = getMaxDigit(arr);
for (int i = 0; i < maxDigit; i++, dev *= 10, mod *= 10) {
// 考虑负数的情况,这里扩展一倍队列数,其中 [0-9]对应负数,[10-19]对应正数 (bucket + 10)
int[][] counter = new int[mod * 2][0];
for (int j = 0; j < arr.length; j++) {
int bucket = ((arr[j] % mod) / dev) + mod;
counter[bucket] = arrayAppend(counter[bucket], arr[j]);
}
int pos = 0;
for (int[] bucket : counter) {
for (int value : bucket) {
arr[pos++] = value;
}
}
}
}
6、排序归纳总结
稳定算法: 插入排序、冒泡排序、归并排序、基数排序
不稳定算法: 希尔排序、快速排序、堆排序、选择排序