大话数据结构
1.数据结构绪论
- 程序设计=数据结构+算法
- 数据对象是数据元素的集合(简称:数据),数据元素又包括数据项,数据项是最小单位了;数据结构:是相互之间存在一种或多种特定关系的数据元素的集合!
- 数据结构:物理结构(数据对象中数据元素之间的相互关系)(集合结构、线性结构、树形结构、图形结构)和逻辑结构(数据的逻辑结构在计算机中的存储形式)(顺序存储结构和链式存储结构)
- 抽象数据类型:抽象是指抽取出事物具有的普遍性的本质,是指一个数学模型以及定义在该模型上的一组操作
2.算法
- 算法:是解决特定问题求解步骤的描述,在计算机中表现为指令的有限序列,并且每条指令表示一个或多个操作。特性:输入、输出、有穷性、确定性和可行性,还要求时间效率高和存储量低
- 算法时间复杂度:大O计法(O(1)常数阶,O(n)线性阶,O(n^2)平方阶,O(logn)对数阶,O(n ^3)立方阶…)
- 算法空间复杂度:若算法执行所需的辅助空间相对于输入数据而言是个常数,则其空间复杂度为O(1)
3.线性表
- 线性表(List):零个或多个数据元素的有限序列。(表中的数据元素类型一致)
3.1顺序存储结构
- 线性表的顺序存储结构:用一段地址连续的存储单元依次存储线性表的数据元素,用数组来实现顺序存储结构;
- 随机存取结构:对于每个线性表位置的存入或者取出数据,都是相等时间,即O(1)
- 线性表的插入和删除:因为插入之后,其后元素都要往后移动一个位置;删除之后,其后元素都要往前移动一个位置;其时间复杂度为O(n);代码实现
3.2链式存储结构(单链表)
-
头指针是指链表指向第一个结点的指针,头指针具有标识作用,常用头指针冠以链表的名字,不论链表是否为空头指针均不为空!
-
单链表的读取:在单链表中第i个元素没法一开始就知道,必须得从头开始找,复杂度O(n);由于单链表结构中没有定义表长因此不能用for来控制循环,用while + “工作指针后移:p=p.next”。
-
单链表的插入和删除:
插入:s.next=p.next; p.next=s;//顺序不能反,否则会断链
删除:p.next=p.next.next;
分为两部分:第一部分就是遍历查找第i个元素,复杂度O(n);第二部分就是插入删除操作,复杂度O(1);对于插入或者删除数据越频繁的操作,单链表的效率优势越明显(一个位置插入多个数据情况) -
单链表的整表创建:头插法+尾插法
public static ListNode CreateListHead(int n){//n为新建链表的长度
ListNode head=new ListNode(-1);
//头插法
for (int i = 0; i < n; i++) {
ListNode node = new ListNode(i);
node.next=head.next;
head.next=node;
}
return head.next;
}
public static ListNode CreateListHead1(int n){
//尾插法
ListNode head = new ListNode(-1);
ListNode p=head;
for (int i = 0; i < n; i++) {
ListNode node = new ListNode(i*2);
p.next=node;
p=node;
}
return head.next;
}
- 顺序存储结构和单链表结构比较:
查找:顺序存储结构O(1),单链表O(n)
插入和删除:顺序结构O(n),单链表在找出某个位置指针后O(1)
空间性能:顺序结构要预分配存储空间,大小不好定;单链表动态的很有优势!
3.3静态链表(了解)
- 用数组来代替指针,来描述单链表;我们把这种用数组描述的链表叫做静态链表!(让数组的元素由两个数据域组成,数据和指针)
- 其本质在于:原来的单链表的指针指向下一个结点在电脑中的物理地址,现在指针指向创建的数组下标,并且连续存储的结点的指针可以任意指向下一个结点从而实现单链表功能!
- 对数组第一个元素和最后一个元素作处理,第一个元素的指针存放备用链表的第一个结点的下标;最后一个元素的指针存放第一个有数值元素的下标,相当于头指针。
- 注:静态链表是没有使用指针的,他的一个数据元素有两个数据域,另一个域存放数组下标(对应于计算机存储物理地址)相当于指针
3.4循环链表、双向链表
- 将单链表中终端结点的指针由空指针改为指向头结点,从而构成循环链表;还增加一个尾指针,指向最后一个结点,来简化查询最后一个结点的复杂度。
- 双向链表是在单链表的的每个结点中再设置一个指向其前驱结点的指针域,故在双向链表中有两个指针:p.next.prior=p=p.prior.next;
- 插入操作:(注意顺序,s为插入结点)(还是以单链表插入为主线插入)
s.prior=p;
s.next=p.next;
p.next.prior=s;
p.next=s; - 删除操作:
p.prior.next=p.next;
p.next.prior=p.prior;
free( p ) ;//释放结点空间
4.栈与队列
4.1栈
- 栈(stack)是仅在表尾进行插入和删除操作的线性表,又被称为先进后出(Last In Fast Out)的线性表
- 进栈出栈变化形式:1、2、3依次进栈,会有哪些出栈次序?321、123、231、213、132;规律?
- 栈的顺序存储结构(数组实现):(入栈+出栈)
/*用数组实现栈,并实现压入弹出方法*/
public static int top=-1;//栈顶指针
public static boolean push(int[] stack,int value){
top++;//语句顺序不能反
stack[top]=value;
return true;
}
public static int pop(int[] stack){
int res=stack[top];
top--;
return res;
}
- 两栈共享空间:使用这种数据结构通常是当两个栈的空间需求有相反关系时,此消彼长;数组有两个端点,array[0]作为栈1的栈底,array[n-1]作为栈2的栈底;当两栈顶指针top1+1=top2时栈满
public static int top1=-1;//栈1的栈顶指针
public static int top2=20;//栈2的栈顶指针
public static boolean flag1=false;//栈1进栈标志位
public static boolean flag2=false;//栈2进栈标志位
public static boolean push(int[] stack,int value){
if (top1+1==top2) return false;
if (flag1==true){
stack[++top1]=value;
}else if(flag2==true){
stack[--top2]=value;
}
return true;
}
public static int pop(int[] stack){
if (flag1==true){
if (top1==-1) return -1;
int temp=stack[top1];
stack[top1]=0;//弹出后将该位置元素清空
top1--;
return temp;
}else if (flag2==true){
if (top2==stack.length) return -1;
int temp=stack[top2];
stack[top2]=0;//弹出后将该位置元素清空
top2--;
return temp;
}else return -1;
}
- 栈的链式存储结构:由于单链表有头指针,所以可以将栈顶放在链表的头部,这样单链表的头指针也就是链表的栈顶指针了!(这个时候不需要头结点了,所以压入时的方法跟头插法有一点区别)
public static int top=-1;//栈顶指针
public static int count=0;//栈的长度
public static void push(ListNode list,int value){
ListNode node=new ListNode(value);
node.next=top;
top=node;
count++;
}
public static int pop(ListNode list){
int temp=top.val;
top=top.next;
count--;
return temp;
}
- 栈的应用——递归:把一个直接调用自己或通过一系列的调用语句间接的调用自己的函数称作递归函数;每个递归定义必须至少有一个条件,满足时递归不再进行,即不再引用自身而是返回值退出
- 斐波那契数列实现(Fibonacci):
//递归方法
Fbi(int n){
if(n<2) return n==0?0:1;
return Fbi(n-1)+Fbi(n-2);
}
//迭代方法
Fbi(int n){
int[] a=new int[n+1];
a[0]=0;
a[1]=1;
for(int i=2;i<n+1;i++){
a[i]=a[i-1]+a[i-2];
}
return a[n];
}
- 在前行阶段,对于每一层递归,函数的局部变量、参数值及返回地址被压入栈中;在退回阶段,位于栈顶的局部变量、参数值和返回地址被弹出,用于返回调用层次中执行代码的其余部分,也就是恢复了调用的状态。
- 栈的应用——四则运算表达式求值:(了解)
4.2队列
- 队列(queue)是只允许在一端进行插入操作,而在另一端进行删除操作的线性表;是一种先进先出(Fast In Fast Out)的线性表,队尾插入,队头删除
- 队列顺序存储:会出现假溢出的情况,即前面还是空的数组后面却已经溢出了(下标越界了);而解决假溢出的方法就是后面满了再从头开始,把队列这种头尾相接的顺序存储结构称为循环队列;其中,队列满的条件是:(rear+1)%QueueSize=front;通用计算队列长度公式:(rear-front+QueueSize)%QueueSize;代码实现
- 队列的链式存储:代码实现
5.串
-
串是由零个或多个字符组成的有限序列,又名字符串
-
串的比较:比较的是字符的ASCII码值(两个规则,如下)
1.当s=“hap”,t=“happy”,则s<t
2.当s=“happen”,t=“happy”,则s<t -
ASCII码值记忆:字符及其对应的ASCII码值
0~9:48 ~57(十进制)
A~Z:65 ~90
a~z:97 ~122 -
子串的定位操作通常称为串的模式匹配,应该算是串中最重要的操作之一!
-
朴素的模式匹配算法:就是对主串的每一个字符作为子串开头,与要匹配的字符串进行匹配。对主串做大循环,每个字符开头做T的长度的小循环,直到匹配成功或全部遍历完成为止
public static int Index(String str1,String str2,int pos){
char[] char1=str1.toCharArray();
char[] char2=str2.toCharArray();
int i=pos;//主串当前位置下标
int j=0;//子串当前位置下标
while (i<char1.length-char2.length+1 && j<char2.length){//减少循环次数,剩余长度没有子串长时不再遍历
if (char1[i]==char2[j]){
i++;
j++;
}else{
i=i-j+1;//退回到上次匹配首位的下一位
j=0;
}
}
if (j>=char2.length) return i-char2.length;//匹配成功
else return -1;//匹配失败
}
- KMP模式匹配算法:朴素匹配模式算法中,主串的i值是不断地回溯来完成的,而KMP模式匹配算法就是为了让没必要的回溯不发生,减少循环次数;解决办法是建一个跟踪子串值变化的数组,每次只让子串判断位置变化,而主串不再回溯!算法实现
6.树
6.1二叉树
-
二叉树(binary tree)每个结点最多有两颗子树,左子树和右子树是有顺序的,要区分开!
-
特殊二叉树:
斜树:所有结点都只有左子树(右子树)的二叉树叫左斜树(右斜树)
满二叉树:所有分支结点都存在左子树和右子树,并且所有叶子都在同一层上
完全二叉树:对一棵具有n个结点的二叉树按层序编号,如果编号为i(i<=n)的结点与同样深度的满二叉树中编号为i的结点在二叉树中位置完全相同,则为完全二叉树
-
二叉树的性质
1.在二叉树的第 i 层上至多有 2i-1 个结点(i>=1)
2.深度为k的二叉树至多有2k-1个结点(k>=1),即等比数列ak=a12k-1的和
3.对任何一棵二叉树,如果其终端结点(叶子结点)数为n0,度为2的结点数为n2,则n0=n2+1,用分支总数联立结点总数求解
4.具有n个结点的完全二叉树的深度为[log2n]+1,([x]表示不大于x的最大整数)
5.层序遍历二叉树(n个结点):若i>1,则其双亲是结点i/2 ;若2i>n,则结点i无左孩子,否则其左孩子是结点2i ;若2i+1>n,则结点i无右孩子,否则其右孩子是结点2i+1 -
二叉树的顺序存储结构:(按完全二叉树的方式存储,层序遍历)
-
二叉链表:为它设计一个数据域和两个指针域,称这样的链表叫做二叉链表
-
遍历二叉树:要求是每个结点被访问一次且仅被访问一次
前序遍历(先根再左再右)、中序遍历(先左再根再右)、后序遍历(先左再右再根)
public class TreeNode {//二叉树结点表示形式
int val;
TreeNode left;
TreeNode right;
public TreeNode(int val){
this.val=val;
}
}
public static void PreTraverse(TreeNode root, ArrayList<Integer> list) {
list.add(root.val);//前序遍历递归实现,并按前序遍历顺序将结点值存入list
if (root.left != null) PreTraverse(root.left, list);
if (root.right != null) PreTraverse(root.right, list);
}
public static void MidTraverse(TreeNode root, ArrayList<Integer> list) {
if (root.left != null) MidTraverse(root.left, list);
list.add(root.val);//中序遍历递归实现
if (root.right != null) MidTraverse(root.right, list);
}
public static void PostTraverse(TreeNode root, ArrayList<Integer> list) {
if (root.left != null) PostTraverse(root.left, list);
if (root.right != null) PostTraverse(root.right, list);
list.add(root.val);//后序遍历递归实现
}
/*迭代实现二叉树前序遍历*/
static void PreTraverse1(TreeNode root, ArrayList<Integer> list) {
if (root == null) return;
Stack<TreeNode> stack = new Stack<>();
//方法一:
/*stack.push(root);
while (!stack.isEmpty()) {
root=stack.pop();
list.add(root.val);
if (root.right != null) stack.push(root.right);
if (root.left != null) stack.push(root.left);
}*/
//方法二:
while (!stack.isEmpty() || root != null) {
while (root != null) {
list.add(root.val);//先访问再入栈
stack.push(root);
root = root.left;
}
root = stack.pop();//如果是null,出栈并处理右子树
root = root.right;
}
}
/*迭代实现二叉树中序遍历*/
static void MidTraverse1(TreeNode root, ArrayList<Integer> list) {
if (root == null) return;
Stack<TreeNode> stack = new Stack<>();
while (root != null || stack.isEmpty()) {
while (root != null) {
stack.push(root);
root = root.left;
}
root=stack.pop();
list.add(root.val);
root = root.right;
}
}
/*迭代实现二叉树后序遍历*/
static void PostTraverse1(TreeNode root, ArrayList<Integer> list) {
if (root == null) return;
Stack<TreeNode> stack = new Stack<>();
stack.push(root);
while (!stack.isEmpty()) {
root = stack.pop();
list.add(root.val);//linkedlist采用addFirst方法,这样就可不用反转list了
if (root.left != null) stack.push(root.left);
if (root.right != null) stack.push(root.right);
}
Collections.reverse(list);
}
/*递归实现二叉树层序遍历*/
static void LevelTraverse(TreeNode root, ArrayList<Integer> list) {
if (root == null) return;
LinkedList<TreeNode> Queue = new LinkedList<>();//创建一个队列,用队列实现层序遍历
Queue.addLast(root);
while (!Queue.isEmpty()) {
TreeNode temp= Queue.removeFirst();
list.add(temp.val);
if (temp.left != null) Queue.addLast(temp.left);
if (temp.right != null) Queue.add(temp.right);
}
}
- 推导遍历结果:由前序遍历和中序遍历可以确定一棵二叉树,由中序遍历和后序遍历也可以确定一棵二叉树;而由前序遍历和后序遍历不能确定一棵二叉树!(用大的思维来看,把一棵树当成一个结点)
- 二叉树的建立
将二叉树中每个结点的空指针引出一个虚结点,从而构造原二叉树的扩展二叉树;扩展二叉树就可以做到一个遍历序列确定一棵二叉树了。(实质还是用遍历的递归方法建立)
public class CreateBinaryTree {
/*构造一棵二叉树*/
static int index = 0;//此值标记传入数组下标,不可缺
/*以前序遍历的方法构造*/
static TreeNode Init(int[] array, int i) {
//一个用于构造二叉树的数组(前序遍历,0元素表示空结点);数组下标
if (array[index] == 0) {
return null;
}
TreeNode node = new TreeNode(array[index]);
node.left = Init(array, ++index);
node.right = Init(array, ++index);
return node;
}
static void PreTraverse(TreeNode root, ArrayList<Integer> list) {
list.add(root.val);//前序遍历递归实现,并按前序遍历顺序将结点值存入list
if (root.left != null) PreTraverse(root.left, list);
if (root.right != null) PreTraverse(root.right, list);
}
public static void main(String[] args) {
TreeNode node = new TreeNode();
int[] arr = {1, 2, 0, 0, 3, 0, 0};
TreeNode root = CreateBinaryTree.Init(arr, 0);
ArrayList<Integer> list = new ArrayList<>();
PreTraverse(root,list);
for (int i = 0; i < list.size(); i++) {
System.out.print(list.get(i)+" ");
}
}
}
- 线索二叉树
指向前驱和后继的指针称为线索,加上线索的二叉链表称为线索链表,相应的二叉树称为线索二叉树;线索二叉树等于是把一棵二叉树转变成了一个双向链表(某种遍历方式下的,前、中、后…),对插入删除查找结点方便了;但同时也导致我们不知道其某一节点的lchild是指向它的左孩子还是前驱,故加入标志位来解决!
线索化的实质就是将二叉链表中的空指针改为指向前驱或后继的线索,由于前驱和后继的信息只有在遍历该二叉树时才能得到,所以线索化的过程就是在遍历的过程中修改空指针的过程。代码实现
7.图
- 线性表中我们把数据元素叫元素,树中将数据元素叫结点,在图中数据元素叫顶点
- 图采用顺序存储和链表存储的物理结构都不合适,顺序存储表现不出其顶点间的逻辑关系,链表存储需要有多个指针域,若按最大连接数设置指针域会浪费空间
- 图的邻接矩阵(Adjancency Matrix)存储方式是用两个数组来表示图。一个一维数组存储图中顶点信息,一个二维数组存储图中的边或者弧的信息;无向图的边数组是一个对称矩阵!
- 图的邻接表存储(了解)
- 图的遍历(了解)
深度遍历和广度遍历代码实现
8.查找
-
查找(Searching)就是根据给定的某个值,在查找表中确定一个其关键字等于给定值的数据元素(或记录)
-
查找概论:
查找表(Search Table)是由同一类型的数据元素(或记录)构成的集合
关键字(key)是数据元素中某个数据项的值,又称键值,可用他标识一个数据元素
若此关键字可以唯一的标识一个记录,则称为主关键字,对那些可以标识多个数据元素的关键字,称为次关键字,主关键字所在的数据项称为主关键码 -
静态查找表:只做查找操作的查找表,主要操作有1、查询某个数据元素是否在查找表中 2、检索某个数据元素的各种属性(用线性表结构来组织数据)
-
动态查找表:在查找过程中同时插入查找表中不存在的数据元素,或者从查找表中删除已经存在的某个数据元素,主要操作有1、查找时插入数据元素 2、查找时删除数据元素(可考虑用二叉排序树的查找技术)
8.1顺序表查找
- 从第一个记录开始一直遍历到最后一个,若遇到要查的数据则返回数据并退出
//相比于for循环,此优化算法更有优势,省去了每次都要判断i是否越界的语句
public int SequentialSearch(int[] arr,int key){
int fast=arr[0];//暂存arr[0]
int i=arr.length-1;
arr[0]=key;//作为哨兵
while(arr[i]!=key) i--;
arr[0]=fast;
if(i>0 || fast==key) return i;
return -1;//查找失败,表中无key数据元素
}
8.2有序表查找
- 折半查找:折半查找(Binary Search)又称二分查找,它的前提是线性表中的记录必须是关键码有序,线性表必须采用顺序存储
- 插值查找:相当于数学当中的等比例关系的应用,前提是数据表中数据有序且分布均匀(mid-low与high-low的位置长度之比等于key-arr[low]与arr[high]-arr[low]的数值长度之比)
- 斐波那契查找:其本质是Fbi[n]=Fbi[n-1]+Fbi[n-2],这样就可以将数据分为两段,然后再按这种规则再分为两段…mid=low+F[k-1]-1
//折半查找的复杂度为O(logn),好过顺序查找的O(n)
Arrays.sort(arr);//传入BinarySearch的数组必须是有序的,这是前提
public int binarySearch(int[] arr,int key){
int low=0;//数组最左边
int high=arr.length-1;//数组最右边
int mid;
while(low<=high){
mid=(low+high)/2;//计算数组的中间位置
//mid=low+(high-low)*(key-arr[low])/(arr[high]-arr[low]);//插值查找,只需在折半查找的代码中改这一行代码
if(key<arr[mid]) high=mid-1;
else if(key>arr[mid]) low=mid+1;
else return mid;
}
return -1;//查找失败,表中无key元素
}
//递归实现
static int binarySearch(int[] arr, int left, int right, int target) {
if (left > right) return -1;
int mid = (left + right) / 2;
if (arr[mid] == target)
return mid;
else if (arr[mid] < target) {
return binarySearch(arr, mid + 1, right, target);
} else {
return binarySearch(arr, left, mid - 1, target);
}
}
8.3线性索引查找
- 对于数据集增长非常快,并且可能无序查找表,采用索引这种数据结构
- 索引就是把一个关键字与它对应的记录相关联的过程,所谓线性索引就是将索引项集合组织为线性结构,也称索引表(x-y的关系,x为索引,y为数据)
- 线性索引的三种:
稠密索引:是指在线性索引中,将数据集中的每个记录对应一个索引项;对于稠密索引这个索引表来说,索引项一定是按照关键码有序的排列
分块索引:为了减少索引项的个数,对数据集进行分块,使其分块有序,然后对每一块建立一个索引项,从而减少索引项的个数;分块有序要满足的条件是块内无序和块间有序
倒排索引:不是由记录来确定属性值,而是由属性值来确定记录的位置
8.4二叉排序树
- 假设查找的数据集为普通的顺序存储,插入和删除的效率还可以,但是由于其无序导致查找效率很低;若数据集为顺序存储的有序线性表,查找用折半等方法效率可以,但是插入和删除效率低;引出一种插入删除效率还不错,并且查找也高效的算法——二叉排序树!
- 二叉排序树(Binary Sort Tree)又称二叉查找树,其左子树值小于根结点,右子树值大于根结点,且左右子树也是二叉排序树;在我们对数据集建立集合时用二叉排序树结构,查找时对它进行中序遍历,就可以得到一个有序序列
- 代码实现
8.5平衡二叉树(AVL树)
- 平衡二叉树(self-Balancing Binary Search Tree),是一种二叉排序树,其中每个结点的左子树和右子树的高度差至多等于1
- 将二叉树上结点的左子树深度减去右子树深度的值称为平衡因子BF(Balance Factor),则平衡二叉树的结点的平衡因子只可能是-1,0,1
- 代码实现
8.6多路查找树(B树)
- 多路查找树(mutil-way search tree),其每个结点的孩子数可以多于两个,且每个结点处可以存储多个元素;由于它是查找树,所有元素之间存在某种特定的排序关系。每个结点可以存储多少元素,以及它的孩子数的多少是非常关键的,在此讲解它的4种特殊形式:2-3树、2-3-4树、B树、B+树
- 2-3树:(了解)它是一棵多路查找树,其中每一个结点都具有两个孩子(称之为2结点)或者三个孩子(称之为3结点);一个2结点包含一个元素和两个孩子(或没有孩子)【注:要有就有两个,不能只有一个孩子】;一个3结点要么没有孩子要么具有3个孩子,并且2-3树中所有的叶子结点都在同一个层次上。
- 2-3-4树:是2-3树的扩展(了解)
- B树(了解)
- B+树(了解)
8.7散列表查找(哈希表)概述
-
直接通过关键字key得到要查找的记录内存存储位置
存储位置 = f (关键字)
通过此函数公式,我们可以通过查找关键字不需要比较就可获得需要的记录的存储位置,这就是一种新的存储技术——散列技术 -
散列技术是在记录的存储位置和它的关键字之间建立一个确定的对应关系f,使得每个关键字key对应一个存储位置f(key)。我们把这种对应关系f称之为散列函数,又称哈希(hash)函数
-
按以上思想,采用散列技术将记录(数据元素)存储在一块连续的存储空间中,这块连续存储空间称为散列表或哈希表(hash table),那么关键字对应的记录存储位置我们称之为散列地址!
-
散列表查找步骤
整个散列过程分为两步:
1、在存储时,通过散列函数计算记录的散列地址,并按此散列地址存储该记录
2、当查找记录时,我们通过同样的散列函数计算记录的散列地址,按此散列地址访问该记录 -
冲突:两个关键字(key1!=key2),但是却有f(key1) =f(key2),这种现象我们称之为冲突,并把key1和key2称为这个散列函数的同一词
-
散列函数构造方法
直接定址法:f(key)=a*key+b
数字分析法:使用关键字的一部分来计算散列存储位置
平方取中法:关键字平方之后取中间几位数作为散列地址
折叠法:分割求和
除留余数法:对于散列表长为m的散列函数公式为:f(key)=key mod p (p<=m),通常p为小于或等于表长的最小质数或不包含20质因子的合数
随机数法:同一个记录存储和查找时用同一个随机因子 -
处理散列冲突的方法
开放定址法
再散列函数法:多个散列函数,一个冲突了用另一个
链地址法:将所有关键字为同义词的记录存储在一个单链表中
公共溢出区法:有冲突的存到另一个散列表中 -
散列表查找实现
public class HashTable {
int hashsize = 0;//哈希表的大小
int count = 0;//哈希表中元素个数
int[] hash;//定义一个数组来作为哈希表
HashTable() {
}
/*构造一个大小为hashsize的哈希表*/
HashTable(int hashsize) {
this.count = hashsize;
this.hashsize = hashsize;
this.hash = new int[hashsize];
for (int i = 0; i < hashsize; i++) {
hash[i] = -32768;
}
}
/*散列函数*/
int Hash(int key) {
return key % this.hashsize;/*除留余数法*/
}
void InsertHash(int key) {
int addr = Hash(key);//求散列地址
while (this.hash[addr] != -32768) {/*如果不为空则冲突*/
addr = (addr + 1) % this.hashsize;/*开放定址法的线性探测*/
}
this.hash[addr] = key;/*找到空位之后将key插入*/
}
/*查询到之后将地址返回*/
int SearchHash(int key) {
int address = Hash(key);
while (this.hash[address] != key) {
address = (address + 1) % this.hashsize;
if (this.hash[address] == -32768 || address == Hash(key))
/*如果循环回到原点*/
return -99;//查询不到
}
return address;
}
}
//测试代码
public static void main(String[] args) {
HashTable table = new HashTable(12);
int[] arr={12,67,56,16,25,37,22,29,15,47,48,34};
for (int i = 0; i < arr.length; i++) {
table.InsertHash(arr[i]);
}
for (int i = 0; i < 12; i++) {
System.out.print(i+" ");
}
System.out.println(" ");
for (int i = 0; i < 12; i++) {
System.out.print(table.hash[i]+" ");
}
System.out.println("************");
for (int i = 0; i < 12; i++) {
if (table.SearchHash(arr[i])!=-99)
System.out.println(arr[i]+" "+table.SearchHash(arr[i]));//查找成功打印地址
else System.out.println(arr[i]+"false");//查找失败,表中无key
}
System.out.println("*******");
System.out.println(table.SearchHash(99));//演示查找不到情况
}
9.排序
- 我们在排序问题中,通常将元素称为记录;在查询的时候也将元素称为记录
- 排序的稳定性
- 内排序和外排序:一个排序过程中将待排序的所有记录放置在内存中,另一个是记录太多时有记录在外存中,要多次读取硬盘(外排序在本文不讨论)
内排序:插入排序、交换排序、选择排序、归并排序
9.1冒泡排序
- 冒泡排序(Bubble Sort)一种交换排序,两两比较相邻记录的关键字,如果反序则交换,直到没有反序的记录为止;复杂度为O(n2)
/**
* @return void
* @Description 冒泡排序,每次相邻两个比较;局部小的往前走,一遍之后最大的沉到最后
* 复杂度为O(n^2),空间复杂度O(1),稳定
* @Param 数组arr
*/
static void bubbleSort(int[] arr) {
for (int i = arr.length - 1; i > 0; i--) {
boolean flag = false;//添加标志位 当数据有序时省略不必要的比较
for (int j = 0; j < i; j++) {
if (arr[j] > arr[j + 1]) {
int temp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = temp;
flag = true;
}
}
if (!flag) break;
}
}
9.2简单选择排序
- 简单选择排序(Simple Selection Sort):用一个指向最小值的指针,选出最小值的地址进行交换,也是两层循环,小循环移动指针,大循环移动数值;复杂度O(n2),但是其性能好于冒泡排序,因为它大大减少了交换的次序
/**
* @return void
* @Description 简单选择排序,固定一个数,从其后找最小数与其交换,减少了交换次数(有点暴力法的味道)
* 时间复杂度O(n^2),空间复杂度O(1),稳定
* @Param 数组arr
*/
static void selectSort(int[] arr) {
for (int i = 0; i < arr.length - 1; i++) {
int min = i;//指向最小值的指针
for (int j = i + 1; j < arr.length; j++) {
if (arr[j] < arr[min]) {
min = j;
}
}
if (i != min)//如果最小值就在最开始标定的位置,则无需交换
swap(arr, i, min);
}
}
static void swap(int[] arr, int i, int j) {
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
9.3直接插入排序
- 直接插入排序(Straight Insertion Sort)的基本操作是将一个记录插入到已经排好序的有序表中,从而得到一个新的、记录数增1的有序表,复杂度O(n2);直接插入排序性能比简单选择排序和冒泡排序好
/**
* @return void
* @Description 直接插入排序,假设前面的序列有序,后面每个数进行插入
* 时间复杂度O(n^2),空间复杂度O(1),稳定
* @Param 数组arr
*/
static void insertionSort(int[] arr) {
for (int i = 1; i < arr.length; i++) {
int temp = arr[i];
int j;
for (j = i; j > 0; j--) {//插入到前面有序的数组中
if (arr[j - 1] <= temp) break;//遇到前一个数不大于它时插入前一个数后面
arr[j] = arr[j - 1];
}
arr[j] = temp;
}
}
9.4希尔排序
- 将相距某个增量的记录组成一个子序列,这样才能保证在子序列内分别进行直接插入排序后得到的结果是基本有序而不是局部有序
/**
* @return void
* @Description 希尔排序,其本质就是插入排序,按增量来分割排序的元素
* @Param 数组arr
*/
static void shellSort(int[] arr) {
int increment = arr.length;
while (increment > 1) {
increment = increment / 3 + 1;//选取的增量序列,可替换
for (int i = increment; i < arr.length; i++) {
int temp = arr[i];
int j;
for (j = i; j > i % increment; j -= increment) {//注意j > i % increment是核心点
if (arr[j - increment] <= temp) break;
arr[j] = arr[j - increment];
}
arr[j] = temp;
}
}
}
9.5堆排序
-
每个结点的值都大于或等于其左右孩子结点的值,称为大顶堆;或者每个结点的值都小于或等于其左右孩子结点的值,称为小顶堆
-
排序思路
1.首先将待排序的数组构造成一个大顶堆,此时,整个数组的最大值就是堆结构的顶端
2.将顶端的数与末尾的数交换,此时,末尾的数为最大值,剩余待排序数组个数为n-1
3.将剩余的n-1个数再构造成大顶堆,再将顶端数与n-1位置的数交换,如此反复执行,便能得到有序数组 -
构建整个堆的时间复杂度为O(n),堆排序的时间 复杂度为O(nlogn);由于构建堆需要比较次数较多,不适合排序序列个数较少情况
public static void swap(int[] arr, int i, int j) {
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
public static void HeapSort(int[] array) {
//建堆过程,i = array.length / 2 - 1为找到最后一个有孩子的父节点
for (int i = array.length / 2 - 1; i >= 0; i--) {
HeapAdjust(array, i, array.length);
}
//排序过程,取大顶堆的堆顶与数组最后一个元素交换,剩下的重新构造大顶堆
for (int j = array.length - 1; j > 0; j--) {
swap(array, 0, j);
HeapAdjust(array, 0, j);
}
}
public static void HeapAdjust(int[] arr, int first, int length) {
int temp = arr[first];
for (int i = first * 2 + 1; i < length; i = i * 2 + 1) {
if (i + 1 < length && arr[i] < arr[i + 1]) i++;//让i先指向子节点中最大的节点
if (arr[i] > temp) { // 如果发现子节点更大,则进行值的交换
swap(arr, first, i);
// 如果子节点更换了,那么,以子节点为根的子树会不会受到影响呢?
// 所以,循环对子节点所在的树继续进行判断
first = i;
} else break;
}
}
9.6归并排序
- 归并排序(Merging Sort)假设初始序列中含有n个记录,则可看成是n个有序的子序列,每个子序列的长度为1,然后两两归并得到[n/2]个长度为2或1的有序子序列;再两两归并,…,直到得到一个长度为n的有序序列为止,这种排序方法称为2路归并排序
- 总的时间复杂度为O(nlogn),空间复杂度为O(n+logn)
public static void MergeSort(int[] array, int first, int end) {
if (first < end) {//当子序列中只有一个元素时结束递归
int mid = (first + end) / 2;//划分子序列
MergeSort(array, first, mid);//对左侧子序列进行递归排序
MergeSort(array, mid + 1, end);//对右侧子序列进行递归排序
Merge(array, first, mid, end);//合并
}
}
//合并两个有序序列变为一个有序序列
public static void Merge(int[] arr, int left, int mid, int right) {
int[] temp = new int[arr.length];
//设置两个指针放在原数组两半,再设置一个指针给暂存数组
int L = left, R = mid + 1, T = left;
while (L <= mid && R <= right) {
//比较找出最小值放入暂存数组
if (arr[L] <= arr[R]) {
temp[T++] = arr[L++];
} else {
temp[T++] = arr[R++];
}
}
//补全未比较到的数值,当两半不一样长时的情况处理
while (L <= mid) temp[T++] = arr[L++];
while (R <= right) temp[T++] = arr[R++];
for (int i = left; i <= right; i++) {//将暂存数组的值复制回原数组
arr[i] = temp[i];
}
}
9.7快速排序
- 快速排序(Quick Sort)的基本思想是:通过一趟排序将待排序记录分割成独立的两部分,其中一部分记录的关键字均比另一部分记录的关键字小,则可分别对这两部分记录继续进行排序,以达到整个序列有序的目的
- 复杂度:在最优情况下,即每次分都是在中间,复杂度为O(nlogn),空间复杂度为O(logn);在最坏情况下,即每次分都在一边,复杂度为O(n2),空间复杂度为O(n)
public static void QuickSort(int[] array, int first, int end) {
if (first < end) {
int p = Partition(array, first, end);//算出枢轴值
QuickSort(array, first, p - 1);//对低子表递归排序
QuickSort(array, p + 1, end);//对高子表递归排序
}
}
public static int Partition(int[] arr, int low, int high) {
int p;
p = arr[low];//用表的第一个记录作为枢轴记录
//循环,返回第一个记录排序好后应该在的位置
//使用两个指针,比较并进行交换
while (low < high) {
while (low < high && arr[high] >= p) high--;
swap(arr, high, low);
while (low < high && arr[low] <= p) low++;
swap(arr, high, low);
}
return low;
}
public static void swap(int[] arr, int i, int j) {
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
9.8排序总结