一、绪论

1、数据结构概论

数据结构研究计算机的操作对象以及他们之间的关系操作

2、算法的定义、特征以及要求

算法:是对特定问题求解步骤的一种描述,它是指令的有限序列,是一系列输入转化为输出的计算步骤。
算法的特征输入输出有穷性确定性可行性
算法的设计要求正确性可读性健壮性效率与低存储量需求

3、算法复杂度

通常我们用时间复杂度空间复杂度来衡量一个算法的优劣。

3.1 时间复杂度

从时间的维度上对算法进行衡量,忽略程序具体的运行时间,从算法整体方面去考虑 ,通常使用:「 大O符号表示法 」,即 T(n) = O(f(n))。利用 大O符号有三个作用:限制忽略数学公式中低阶项产生的误差;限制由于忽略程序中对运行时间贡献小的部分产生的错误;允许我们按照算法总运行时间的上界对算法进行分类。

常见的时间复杂度量级有:常数阶O(1)O(1)O(1)、对数阶O(logN)O(logN)O(logN)、线性阶O(n)O(n)O(n)、线性对数阶O(nlogN)O(nlogN)O(nlogN)、平方阶O(n2)O(n²)O(n2)、立方阶O(n3)O(n³)O(n3)、K次方阶O(nk)O(n^k)O(nk)、指数阶O(2n)O(2^n)O(2n)

3.2 空间复杂度

空间复杂度是对一个算法在运行过程中临时占用存储空间大小的一个量度,同样反映的是一个趋势,我们用 S(n) = O(f(n))来定义。

常见的空间复杂度量级有:O(1)O(1)O(1)、O(n)O(n)O(n)、O(n2)O(n²)O(n2)

二、线性表

1、顺序存储结构

逻辑上相邻的数据元素,在物理上也是相邻的。

1.1 修改——修改第iii个位置的元素

核心语句:

data[i]=value; //将数组data中索引为i的元素修改为value

时间复杂度:O(1)O(1)O(1)

1.2 插入——在第iii个位置前插入一个元素

核心语句:

for(int j=n;j>=i;j--)
{data[j+1]=data[j];
}
data[i]=value;
n++;

时间复杂度:O(n)O(n)O(n)

1.3 删除——删除第iii个位置的元素


核心代码:

for(int j=i;j<n;j++)
{data[j]=data[j+1];
}
n--;

时间复杂度:O(n)O(n)O(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;}//链表存储的队列,理论上不会满,人为限制最大Maxpublic 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、二叉树的性质

  • 二叉树的第iii层上至多有2i−12^{i-1}2i−1个结点(i>0i>0i>0)
  • 深度为kkk的二叉树至多有2k−12^k-12k−1个结点
  • 任何一个二叉树,为2的节点数和度为0的叶子结点之间的关系为:n0=n2+1n_0=n_2+1n0​=n2​+1
    推导过程:{n0+n1+n2=n(ni为有i个叶子结点的个数)n−1=总度数=0∗n0+1∗n1+2∗n2推导过程:\begin{cases} n_0+n_1+n_2 = n(n_i为有i个叶子结点的个数)\\ n-1=总度数=0*n_0+1*n_1+2*n_2 \end{cases} 推导过程:{n0​+n1​+n2​=n(ni​为有i个叶子结点的个数)n−1=总度数=0∗n0​+1∗n1​+2∗n2​​
  • 具有nnn个结点的完全二叉树深度为⌊log2n⌋+1\left \lfloor log_2n \right \rfloor+1⌊log2​n⌋+1
  • 对完全二叉树用iii编号,对于iii节点,其左子为2i2i2i,右子为2i+12i+12i+1,其双亲为⌊i/2⌋\left \lfloor i/2 \right \rfloor⌊i/2⌋,同理最后一个非叶子节点编号为⌊n/2⌋\left \lfloor n/2 \right \rfloor⌊n/2⌋, i=1,2,3,...i=1,2,3,...i=1,2,3,...

2、树和二叉树的相互转换

树转化为二叉树:

二叉树转化为树:

3、树和森林的相互转换

森林转化为二叉树:

二叉树还原为森林:

4、树的存储方法

4.1 左孩子右孩子表示法(二叉树)


例子:

4.2 双亲表示法


例子:

4.3 孩子表示法


4.4 孩子兄弟表示法

5、二叉树的遍历

5.1 构建二叉树

通过一个先序的二维数组的形式构造二叉树,二叉树空缺的部分用0表示,如[1,2,3,0,4,5,0,0,6,0,0,7,0,0,8,0,9,10,0,0,0][1,2,3,0,4,5,0,0,6,0,0,7,0,0,8,0,9,10,0,0,0][1,2,3,0,4,5,0,0,6,0,0,7,0,0,8,0,9,10,0,0,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为先序遍历数组,空子为0TreeNode 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; //初始化之前,需要将索引置为-1return 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为空则返回falseif(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 哈夫曼树的构造方法:

算法流程:

  1. 给定一个元素集合array
  2. 分别找到最小的两个元素a,b
  3. 新建一个值为c(c=a+b)的二叉树结点,左右子分别指向值为a,b的结点
  4. 集合array删除a,b元素
  5. 如果array不为空,新增c元素
  6. 重复步骤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 哈夫曼编码

哈夫曼编码

  1. 以电文中的字符作为叶子结点构造二叉树。
  2. 将二叉树中结点引向其左孩子的分支标 ‘0’,引向其右孩子的分支标 ‘1’。
  3. 每个字符的编码即为从根到每个叶子的路径上得到的 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的上级为 xelse                             //否则{if(rank[x]==rank[y]) rank[y]++;    //如果 x的高度和 y的高度相同,则令 y的高度加1pre[x]=y;                        //让 x的上级为 y}return true;                        //返回 true,表示合并成功}
}

3、总结

  1. 用来代表某集合的元素称为此集合的代表元
  2. 一个集合内的所有元素组织成以代表元为根的树形结构。
  3. 对于每一个元素 x,pre[x] 存放 x 在树形结构中的前驱节点(如果 x 是根节点,则令pre[x] = x)。
  4. 对于查找操作,可以沿着pre[x]不断在树形结构中向上移动,直到查找到代表元。
  5. 对于合并操作,只需将一个树型结构的代表元作为另一个树型结构的子结构。

六、图

1、概念

图的定义:由顶点的有穷非空集合和顶点之间边的集合组成。记为:
G(V,E)G(V,E) G(V,E)
其中,GGG 表示一个图,VVV是图中的顶点的集合,EEE是图 中边的集合。

图的特点:是一种较线性表和树更加复杂的数据结构,在图形结构中,结点之间的关系可以是任意的,图中任意两个数据元素之间都可能相关。

2、图的分类

2.1 无向图

  若顶点viv_ivi​到vjv_jvj​之间的边没有方向,则称这条边为无向边,用无序偶对(vi,vj)(v_i,v_j)(vi​,vj​)来表示。
  如果图中任意两个顶点之间的边都是无向边,则称该图为无向图,无向图顶点的边数叫做

无向图示例:

2.2 有向图

  若从顶点viv_ivi​到vjv_jvj​的边有方向,则称这条边为有向边,也称为。用有序偶来表示,viv_ivi​称为弧尾(Tail),vjv_jvj​称为弧头(Head)。
  如果图中任意两个顶点之间的边都是有向边,则称该图为有向图(Directed graphs)。有向图顶点分为入度(箭头朝自己)和 出度(箭头朝外)。

有向图示例:

2.3 简单图

定义:在图中,若不存在顶点到其自身的边,且同一条边不重复出现,则称这样的图为简单图

如下所示的两个图就不属于简单图:

2.4 完全无向图

在无向图中,如果任意两个顶点之间都存在边,则称该图为无向完全图

如下图所示即为一个无向完全图:

2.5 完全有向图

在有向图中,如果任意两个顶点之间都存在方向互为相反的两条弧,则称该图为有向完全图

如下图所示即为一个有向完全图:

3、图的存储结构

3.1 邻接矩阵表示法

设图有n个顶点,则图表示为A.Edge[n][n]
第iii行表示:以iii为入度
第iii列表示:以iii为出度

以邻接矩阵表示为:

注:0表示两个顶点之间没有弧,1表示两个顶点之间相连,权值为1
优点: 容易实现各种图的操作
缺点: 空间效率低 O(n2)O(n^2)O(n2)

数据结构代码:

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; //访问标志位置1for(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;//入队后访问标志位置1while (!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-Bgraph2.addArc(0,'C',1); //A-Cgraph2.addArc(0,'D',1); //A-Dgraph2.addArc(1,'A',1); //B-Agraph2.addArc(1,'E',1); //B-Egraph2.addArc(1,'F',1); //B-Fgraph2.addArc(2,'A',1); //C-Agraph2.addArc(3,'A',1); //D-Agraph2.addArc(3,'F',1); //D-Fgraph2.addArc(4,'B',1); //E-Bgraph2.addArc(5,'B',1); //F-Bgraph2.addArc(5,'D',1); //F-Dfor(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-Bgraph1.addArc(1,0,1);graph1.addArc(1,4,1); //B-Egraph1.addArc(4,1,1);graph1.addArc(1,5,1); //B-Fgraph1.addArc(5,1,1);graph1.addArc(5,3,1); //F-Dgraph1.addArc(3,5,1);graph1.addArc(0,3,1); //A-Dgraph1.addArc(3,0,1);graph1.addArc(0,2,1); //A-Cgraph1.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; //访问标志位置1for(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、图的应用——最小生成树

生成树: 无向图中相互连通,且nnn个顶点只有n−1n-1n−1条边的联通子图叫生成树。

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的上级为 xelse                             //否则{if(rank[x]==rank[y]) rank[y]++;    //如果 x的高度和 y的高度相同,则令 y的高度加1pre[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; //返回边结构链表
}

时间复杂度: O(eloge)O(eloge)O(eloge) 【查找最小边复杂度O(e)O(e)O(e),查找是否属于同一个树,即是否连通复杂度O(loge)O(loge)O(loge)】

适用: 适用于求稀疏图的最小生成树

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-Bgraph1.addArc(1,0,6);graph1.addArc(0,2,1); //A-Cgraph1.addArc(2,0,1);graph1.addArc(0,3,5); //A-Dgraph1.addArc(3,0,5);graph1.addArc(1,4,3); //B-Egraph1.addArc(4,1,3);graph1.addArc(1,2,5); //B-Cgraph1.addArc(2,1,5);graph1.addArc(2,3,5); //C-Dgraph1.addArc(3,2,5);graph1.addArc(2,4,6); //C-Egraph1.addArc(4,2,6);graph1.addArc(2,5,4); //C-Fgraph1.addArc(5,2,4);graph1.addArc(3,5,2); //D-Fgraph1.addArc(5,3,2);graph1.addArc(4,5,6); //E-Fgraph1.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

时间复杂度: 邻接矩阵表示O(V2)O(V^2)O(V2) 邻接表表示O(elogV)O(elogV)O(elogV)

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); //将距离最小的节点加入pathsvisited[node]=1; //将该最小节点访问标志位置1for(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

算法时间复杂度:对于单个点到其他各点是O(n2)O(n^2)O(n2),如果要求图中所有点的最短距离,则时间复杂度为O(n3)O(n^3)O(n3)

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

时间复杂度: O(n3)O(n^3)O(n3)

七、查找

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=(1+n)/2ASL=(1+n)/2ASL=(1+n)/2
顺序查找的特点: 算法简单,对顺序表或者链表结构都适用,缺点是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;
}

ASL≈log2nASL≈log_2nASL≈log2​n
特点 :查找的表必须是有序表,且存储结构必须是顺序存储

2.3 分块查找

思路: 先让数据分块有序,即分成若干个子表,要求每个子表中的元素都比后一块中的数值小(子表内未必有序)。然后将各子表中的最大关键字构成一个索引表,表中还要包含每个子表的起始地址。在块内进行顺序查找,块间进行折半查找。
特点: 块内无序,块间有序
查找: 块间折半查找,块内顺序查找
图示:

ASL=Lb+LwASL=L_b+L_wASL=Lb​+Lw​ (LbL_bLb​是索引表查找的ASL,LwL_wLw​是块内查找的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 直接插入排序

排序思想: 在已形成的有序表中线性查找,并在适当位置插入,把原来位置上的元素向后顺移。

时间效率: 因为在最坏情况下,所有元素的比较次数总和为(0+1+…+n−1)→O(n2)(0+1+…+n-1)→O(n^2)(0+1+…+n−1)→O(n2)。最好情况是每次都不移动复杂度为 O(n)O(n)O(n),其他情况下也要考虑移动元素的次数。 故时间复杂度为 O(n2)O(n^2)O(n2)
空间效率: 仅占用 1 个缓冲单元——O(1)O(1)O(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 折半插入排序

排序思想: 既然子表有序且为顺序存储结构,则插入时采用折半查找定可加速。
优点: 比较次数大大减少,全部元素比较次数仅为 O(nlog2n)O(nlog_2n)O(nlog2​n)。
时间效率: 虽然比较次数大大减少,可惜移动次数并未减少, 所以排序效率仍为O(n2)O(n^2)O(n2) 。
空间效率: 仍为 O(1)O(1)O(1)
稳 定 性: 稳定
补充: 若记录是链表结构,用直接插入排序无需移动元素,时间效率更高!但是单链表结构无法实现“折半查找”

1.3 2-路插入排序

排序思想: 前面的插入排序每次插入元素的时候都会移动较多的元素,二路插入排序对其进行了改善。思路是:以第一个元素作为比较元素,后面所有大于该元素的数全部放在前面,所有小于元素的数放在后面,大于或小于部分的元素在插入的时候使用直接插入排序来保证有序,当所有元素分配好后,其实数组已经变成两个有序区,再组合好就完成排序了。

1.3 表插入排序

排序思想: 顺序存储结构中,给每个记录增开一个指针分量,在排序过程中将指针内容逐
个修改为已经整理(排序)过的后继记录地址。
优点: 在排序过程中不移动元素,只修改指针。此方法具有链表排序和地址排序的特点
时间效率: 无需移动记录,只需修改指针值。但由于比较次数没有减少,故时间效率仍为 O(n2)O(n^2)O(n2)。
空间效率: 空间效率肯定低,因为增开了指针分量(但在运算过程中没有用到更多的辅助单元)
稳 定 性: 稳定

1.4 希尔(shell)排序

排序思想: 希尔排序是把记录按一定增量分组,对每组使用直接插入排序算法排序;随着增量逐渐减少,每组包含的关键词越来越多,当增量减至1时,整个记录恰被分成一组,算法便终止。
时间效率: O(n1.25)O(n^{1.25})O(n1.25)~O(1.6n1.25)O(1.6n^{1.25})O(1.6n1.25) ——由经验公式得出
空间效率: O(1)O(1)O(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 冒泡排序

基本思路: 每趟不断将记录两两比较,并按“前小后大”(或“前大后小”)规则交换。
优点: 每趟结束时,不仅能挤出一个最大值到最后面位置,还能同时部分理顺其他元素;一
旦下趟没有交换发生,还可以提前结束排序。
前提: 顺序存储结构
![在这里插入图片描述](https://img-blog.csdnimg.cn/a11b843723a949e19674dd57bd07e139.png x220)
时间效率: O(n2)O(n^2)O(n2)—因为要考虑最坏情况
空间效率:O(1)O(1)O(1) —只在交换时用到一个缓冲单元
稳定性: 稳定
代码:

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 快速排序

基本思路: 从待排序列中任取一个元素 (例如取第一个) 作为中心,所有比它小的元素一律
前放,所有比它大的元素一律后放,形成左右两个子表;然后再对各子表重新选择中心元素
并依此规则调整,直到每个子表的元素只剩一个。此时便为有序序列了
前提: 顺序存储结构

时间效率: O(nlog2n)O(nlog_2n)O(nlog2​n)——因为每趟确定的元素呈指数增加
空间效率:O(log2n)O(log_2n)O(log2​n) ——因为递归要用栈(存每层 low,high 和 pivot)
稳定性: 不稳定
代码:

public static int partion(int[] array,int low,int high){int pivot = array[low]; //以第一个元素作为pivotwhile (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 简单选择排序

排序思想: 每经过一趟比较就找出一个最小值,与待排序列最前面的位置互换即可。
前提: 顺序存储结构
图示:

时间效率: O(n2)O(n^2)O(n2) ——移动次数少,但是比较次数多
空间效率:O(1)O(1)O(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 个较小者之间再进行两两比较,…,如此重复,直到选出最小关键字的记录为止。
优点: 减少比较次数,加快排序速度
缺点: 空间效率低
时间效率: O(nlog2n)O(nlog_2n)O(nlog2​n)
空间效率:O(log2n)O(log_2n)O(log2​n)

3.3 堆排序

时间效率: O(nlog2n)O(nlog_2n)O(nlog2​n)。因为整个排序过程中需要调用 n-1 次 heapAdjust( )算法,而算法本身耗时为log2nlog_2nlog2​n;
空间效率: O(1)O(1)O(1)。仅在第二个 for 循环中交换记录时用到一个临时变量。
稳定性: 不稳定。
优点: 对小文件效果不明显,但对大文件有效。
图示:

代码:

public static void heapSort(int[] array){for(int i=array.length-1;i>0;i--){heapAdjust(array,i); //按大根堆对数组进行调整,每次循环调整的范围从末尾-1swap(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)的阶段则将分的阶段得到的各答案"修补"在一起,即分而治之)。
图示:

时间效率: O(nlog2n)O(nlog_2n)O(nlog2​n)——按二叉树的方式分层
空间效率: O(nlog2n)O(nlog_2n)O(nlog2​n)——每层都需要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
图示:

时间效率: O(d∗(n+r))O(d*(n+r))O(d∗(n+r)) ——其中,d 为位数,r 为基数,n 为原数组个数。
空间效率: O(n+r)O(n+r)O(n+r)
稳定性: 稳定。
代码:

// 获取最大值的位数,用来确定桶的最大容量
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、排序归纳总结

稳定算法: 插入排序、冒泡排序、归并排序、基数排序
不稳定算法: 希尔排序、快速排序、堆排序、选择排序

数据结构(java版)相关推荐

  1. 数据结构(java版)SortedSeqList(排序顺序表)

    SortedSeqList(排序顺序表) 代码部分: public class SortedSeqList<T extends Comparable<? super T>> e ...

  2. 数据结构java版txt,图解数据结构:使用Java

    图解数据结构:使用Java 下载 mobi epub pdf ☆☆☆☆☆ 胡昭民 著 下载链接在页面底部 发表于2021-03-10 图书介绍 出版社: 清华大学出版社 ISBN:9787302402 ...

  3. 数据结构(Java版 2022-10-30)

    第一章:算法介绍 数据结构与算法面试题" 一.字符串匹配问题:有一个字符串str1="计算机科学与技术学院欢迎您!" 和另一个字符串 str2="计算机科学与技 ...

  4. 数据结构(Java版2022-10-29)

    第一章:算法介绍 数据结构与算法面试题" 一.字符串匹配问题:有一个字符串str1="计算机科学与技术学院欢迎您!" 和另一个字符串 str2="计算机科学与技 ...

  5. 数据结构Java版实验五_实验五数据结构综合应用 20162310

    分析系统架构 Sprite精灵类 ISprite精灵类是所有类的父类 CombatAircraft战斗机类 首先确保战斗机完全位于Canvas范围内,每隔7帧发射单发黄色子弹. protected v ...

  6. 数据结构java版 大学_数据结构(Java版)

    "数据结构"是计算机科学与技术专业.软件工程专业甚至于其它电气信息类专业的重要专业基础课程.它所讨论的知识内容和提倡的技术方法,无论对进一步学习计算机领域的其它课程,还是对从事大型 ...

  7. 数据结构java版之 栈的应用一

    上一篇我们自定义了栈和队列.本篇使用栈结构来完成一个功能,看看他的应用.会分两篇讲解.本篇需求:设计一个栈结构,实现字符串的反转,字符串不包括汉子. 过程如下,因为代码做了很详细的解释.没有必要再去讲 ...

  8. 数据结构java版之《数组》

    本篇文章,使用java语言,封装一个数组工具类.不使用系统提供的api. 包括一些数组的增删改查,有序添加,二分查找等功能. package ch01;public class MyArray {// ...

  9. 数据结构Java版之红黑树(八)

    红黑树是一种自动平衡的二叉查找树,因为存在红黑规则,所以有效的防止了二叉树退化成了链表,且查找和删除的速度都很快,时间复杂度为log(n). 什么是红黑规则? 1.根节点必须是黑色的. 2.节点颜色要 ...

  10. 数据结构Java版之排序算法(二)

    排序按时间复杂度和空间复杂度可分为 低级排序 和 高级排序 算法两种.下面将对排序算法进行讲解,以及样例的展示. 低级排序:冒泡排序.选择排序.插入排序. 冒泡排序: 核心思想,小的数往前移.假设最小 ...

最新文章

  1. Quartz.net 定时任务矿建Demo
  2. 004_Mysql数据库的CRUD的操作
  3. 启动ubuntu无反应_推荐一款优秀的Python IDE以及在Ubuntu下的安装
  4. MapReduce-Reduce端join操作-步骤分析
  5. 1097 Deduplication on a Linked List (25 分)_35行代码AC
  6. Vuex 模块化与项目实例 (2.0)
  7. Springboot校园二手市场实战开发
  8. SQuAD2.0来了!新增5万人工撰写问题,且不一定有答案 | ACL最佳短论文
  9. 多边多面形成体_Nature Comm | 中科院分子植物卓越中心巫永睿团队揭示类胡萝卜素影响玉米硬质胚乳形成的新机制...
  10. 天大 ACM 1090. City hall
  11. Android 四大组件学习之ContentProvider一
  12. 带鉴权信息的SIP呼叫
  13. 用户和计算机硬盘系统的接口,硬盘接口类型,教您怎么看硬盘接口的类型
  14. 第三阶段应用层——1.8 数码相册—在LCD上显示JPG图片
  15. android修改图标
  16. KGB知识图谱凭借OCR文字识别突破文档解析局限
  17. MATLAB 自动数独求解器(导入图片自动求解)
  18. ui设计培训机构内课程包括哪些板块|优漫动游
  19. 搜索之BM25和BM25F模型
  20. 看史上最牛的夫妻生活协议书

热门文章

  1. Maple问题解决(一):解决绘图输出(打印)设置之后绘图不再在页面显示的问题
  2. js —— undefined与出错
  3. Vue2知识点(四)(使用Vue CLI知识点(续集))
  4. 【致青春】开垦出的IT路
  5. sklearn中svr(支持向量机回归)
  6. 机器学习算法之KNN
  7. 魅族手机鸿蒙系统,魅族宣布接入鸿蒙系统,被网友吐槽蹭热点
  8. Https 公钥私钥交换过程
  9. TLS握手协议详解下
  10. c语言用取余判断奇偶数