第一章 稀疏数组与队列
一、稀疏数组
1.概念
一个数组中大部分元素为0或者为同一个值,可以用稀疏数组来保存数组。
2.稀疏数组建立
/*
建立与遍历稀疏数组
*/
int[][] sparse = new int[5][5];//动态初始化
sparse[0][1] = 1;
sparse[2][3] = 2;
sparse[4][4] = 2;
for(int[] i : sparse){//(类型 形参 : 对象)
for(int data : i){
System.out.printf("%d\t",data);//(\t)是制表符
}
System.out.println();
}
3.二维数组转化为稀疏数组
1.先遍历稀疏数组,记录有多少个非零的值
//将二维数组转化为稀疏数组
//1.首先遍历二维数组,得到非零的个数
int sum = 0;
for(int i = 0; i < 5 ; i++){
for (int j = 0; j < 5; j++) {
if(sparse[i][j] != 0){
sum++;
}
}
}
//创建稀疏数组
int[][] sparseArr = new int[sum+1][3];//sum指的是有几个非零值,
//而3指的是该数是存于第几行、第几列、值为几
sparseArr[0][0] = 5;
sparseArr[0][1] = 5;
sparseArr[0][2] = sum;//稀疏数组索引0对应的一维数组表达的是原数组是
//几行几列的,且有几个不同的值
//遍历二维数组,将非零值装进稀疏数组中
int count = 0;
for (int i = 0; i < 5; i++) {
for (int j = 0; j < 5; j++) {
if(sparse[i][j] != 0){
count++;
sparseArr[count][0] = i;//放入行值
sparseArr[count][1] = j;//放入列值
sparseArr[count][2] = sparse[i][j];//放入数值
}
}
}
//输出得到的稀疏数组
for(int[] row : sparseArr){
for(int data : row){
System.out.printf("%d\t",data);
}
System.out.println();
}
注:sum+1是因为稀疏数组的最外层的一维数组索引为0的位置要存放稀疏数组的属性
故要给sum+1
4.稀疏数组转化为二维数组
//将稀疏数组转化为二维数组
int[][] arr = new int[sparseArr[0][0]][sparseArr[0][1]];//根据属性建立二维数组
for (int i = 1; i < sparseArr.length; i++) {
arr[sparseArr[i][0]][sparseArr[i][1]] = sparseArr[i][2];//将非零值放入新的二维数组
}
//遍历二维数组
for(int[] row : arr){
for(int data : row){
System.out.printf("%d\t",data);
}
System.out.println();
}
二、队列
1.概念
队列是一个有序链表,可以用数组或是链表来实现
2.特点
队列本质是数组,其指针都是由int型来表示的,队列队列所以先来的要先走(先入先出)
3.队列的创建
可以实现一个队列类:队列的首尾指针rear和front初始值都为-1,front 会随着数据输出而改变,即输出时+1,而 rear 则是随着数据输入而改变,即输入时+1.再添加相应队列的方法
class ArrayQueue{
private int maxSize;
private int front;
private int rear;
private int []arr;
//创建队列的构造器
public ArrayQueue(int arrMaxSize){
maxSize=arrMaxSize;
front=-1;
rear=-1;
arr= new int[maxSize];
}
//判断队列是否满
public boolean isFull(){
return rear==maxSize-1;
}
//判断队列是否空
public boolean isEmpty(){
return rear==front;
}
//添加数据的方法
public void addQueue(int n){
if(isFull()){
System.out.println("队列已满,不能添加");
return;
}
rear++;
arr[rear]=n;
}
//取数据的方法
public int getQueue(){
if(isEmpty()){
throw new RuntimeException("队列为空,无法读取数据");
}
front++;
return arr[front];
}
//显示队列的所有数据
public void showQueue() {
if(isEmpty()){
System.out.println("队列为空,无法读取数据");
return;
}
for(int i=0;i<arr.length;i++){
System.out.println(arr[i]);
}
}
//显示队列的头数据
public int headQueue() {
// 判断
if (isEmpty()) {
throw new RuntimeException("队列空的,没有数据~~");
}
return arr[front + 1];
}
}
注:队列有个缺点,就是用一次之后就不能用了,所以我们需要环形数组
4.环形队列的创建
由于队列是环形的,首尾指针rear和front的初始值都是0
注:1. 判断队列满的条件:(rear + 1) % maxSize == front;
2. 队列中的有效数据:(rear + maxSize - front) % maxSize;
class CircleArray {
private int maxSize;
private int front;
private int rear;
private int[] arr;
public CircleArray(int arrMaxSize) {
maxSize = arrMaxSize;
arr = new int[maxSize];
rear = 0;
front = 0;
}
// 判断队列是否满
public boolean isFull() {
return (rear + 1) % maxSize == front;
}
// 判断队列是否空
public boolean isEmpty() {
return rear == front;
}
// 添加数据的方法
public void addQueue(int n) {
if (isFull()) {
System.out.println("队列已满,不能添加");
return;
}
arr[rear] = n;
rear = (rear + 1) % maxSize;
}
//取数据的方法
public int getQueue() {
if (isEmpty()) {
throw new RuntimeException("队列为空,无法读取数据");
}
// 1.先把front对应的值保留到i一个临时变量
// 2.将front后移,考虑取模
// 3.将临时保存的变量返回
int value = arr[front];
front = (front + 1) % maxSize;
return value;
}
// 显示队列的所有数据
public void showQueue() {
if(isEmpty()){
System.out.println("队列为空,无法读取数据");
return;
}
for (int i = front; i < front + size() ; i++) {
System.out.print(arr[i % maxSize]);
}
}
// 求队列的有效数据个数
public int size() {
return (rear + maxSize - front) % maxSize;
}
// 显示队列的头数据, 注意不是取出数据
public int headQueue() {
// 判断
if (isEmpty()) {
throw new RuntimeException("队列空的,没有数据~~");
}
return arr[front];
}
第二章 链表
一、单链表概念
链表链表就是一个带有链接的表。
其特点如下:
- 链表是以节点的方式来存储,是链式存储。
- 每个节点包含 data 域, next 域:指向下一个节点。
- 如图链表的各个节点不一定是连续存储,要看next域指向。
- 链表分带头节点的链表和没有头节点的链表,根据实际的需求来确定
二、单链表的增删改和遍历
1.单链表节点的插入
//向单链表中添加节点
public void add(Node newNode){
Node temp = head;
while(true){
if(temp.next == null){
break;
}
temp = temp.next;
}
temp.next = newNode;
}
//考虑排序的添加节点
public void addByOrder(Node newNode){
Node temp = head;
boolean flag = false;
while(true){
if(temp.next == null){
break;
}
if(temp.next.no > newNode.no){
break;
}else if(temp.next.no == newNode.no){
flag = true;
break;
}
temp = temp.next;
}
if(flag){
System.out.println("该节点"+newNode.no+"已经存在");
}else{
newNode.next = temp.next;//把地址先接好
temp.next = newNode;//再把节点接好
}
}
注:
- temp是临时指针,指的是当前节点,通过temp指针来辅助遍历,寻找节点
- 第一种插入不需要考虑排序,即找到节点的位置插入即可
- 考虑排序的插入方法时,需要比较节点的序号,所以在这里面添加一个标志,标志添加的编号是否存在,默认为 false
2.单链表节点的修改
//修改链表的节点
public void upDate(Node node){
if(head.next == null){
System.out.println("链表为空,无法修改");
}
Node temp = head.next;
boolean flag = false;
while(true){
if(temp == null){
break;
}
if(temp.no == node.no){
flag = true;
break;
}
temp = temp.next;
}
if(flag){
temp.name = node.name;
}else {
System.out.println("该节点"+ node.no + "未找到");
}
}
注:
- 链表要是空的,就没法修改,只是第一,要写在前面
- 首先,头节点不能修改,所以当前的辅助指针一开始就是指向头结点的下一节点
- 修改,要先找到,所以没找到的要处理,且没找到的就改不了flag标志,flag是用来确定node节点是否找到,找到就修改节点
3.单链表结点的删除
//删除节点
public void delete(int no){
if(head.next == null){
System.out.println("链表为空,无法删除");
}
Node temp = head.next;//头节点不能删除
boolean flag = false;
while(true){
if(temp == null){
break;
}
if(no == temp.no){
flag = true;
break;
}
temp = temp.next;
}
if(flag){
temp.next = temp.next.next;//把当前节点后两个节点的地址给当前节点后一个节点附上
}else {
System.out.println("该节点未找到");
}
}
注:
- 同样地,链表为空就删除不了
- temp指针处在要删除的结点的前一个,所以要把当前节点后两个节点的地址赋给当前节点后一个节点
4.单链表的遍历
public void list(){
if(head.next == null){
System.out.println("链表为空,不能遍历");
return;
}
Node temp = head.next;
while(true){
if(temp == null){
break;
}
System.out.println(temp);//此处的temp其实是temp.toString()
temp = temp.next;
}
}
三、双向链表概念
1.单双链表的区别
- 单向链表,查找的方向只能是一个方向,而双向链表可以向前或者向后查找。
- 单向链表不能自我删除,需要靠辅助节点 ,而双向链表,则可以自我删除,所以前面我们单链表删除时节点,总是找到 temp,temp 是待删除节点的前一个节点(认真体会).
2.双向链表节点的插入
//添加节点
public void add(D_node newnode){
D_node temp = head;
while(true){
if(temp.next == null){
break;
}
temp = temp.next;
}
temp.next = newnode;
newnode.pre = temp;
}
注:temp.next = newnode;
和newnode.pre = temp;
注意是节点赋给指针
3.双向链表节点的修改
//修改节点
public void update(D_node node){
if(head.next == null){
System.out.println("链表为空~");
return;
}
D_node temp = head.next;
boolean flag = false;
while (true) {
if(temp == null){
break;
}
if(temp.no == node.no){
flag = true;
break;
}
temp = temp.next;
}
if(flag){
temp.name = node.name;
}else {
System.out.println("未找到要修改的节点");
}
}
修改节点的思路和单向链表一样
4.双向链表节点的删除
//删除节点
public void delete(int no){
if(head == null){
System.out.println("链表为空,不能删除");
return;
}
D_node temp = head.next;
boolean flag = false;
while (true) {
if(temp==null){
break;
}
if(temp.no == no){
flag = true;
break;
}
temp = temp.next;
}
if(flag){
temp.next.pre = temp.pre;
temp.pre.next = temp.next;//要把当前节点的前后节点往指针上接,
//节点是主动接的,所以节点放在等号右边
}else{
System.out.println("未找到要删除的节点");
}
}
注:temp.next.pre = temp.pre;和temp.pre.next = temp.next;
注意是节点赋给指针,把节点接到指针上
四、环形链表
1.概念
将首尾相接的节点构成的链表叫做环形链表,由约瑟夫问题引出,小孩围成一圈,将每个孩子看作一个节点,进行增添删除和遍历
2.环形链表的建立
class SingleCircleLinkedList{
private Boy1 frist = null;
public void addBoy(int num){
if(num<1){
throw new RuntimeException("输入的数太小,不能构成环形链表");
}
Boy1 curboy = null;
for (int i = 1; i <= num ; i++) {
Boy1 boy = new Boy1(i);
if(i == 1){
frist = boy;
frist.setNext(frist);
curboy = frist;
}else{
curboy.setNext(boy);
boy.setNext(frist);
curboy = boy;
}
}
}
public void list(){
if(frist == null){
System.out.println("链表为空,不能遍历");
return;
}
Boy1 curboy = frist;
while(true){
System.out.printf("小孩的编号 %d \n", curboy.getNo());
if (curboy.getNext() == frist){
break;
}
curboy = curboy.getNext();
}
}
}
class Boy1{
private int no;
private Boy1 next;
public Boy1(int no){
this.no = no;
}
public int getNo() {
return no;
}
public void setNo(int no) {
this.no = no;
}
public Boy1 getNext() {
return next;
}
public void setNext(Boy1 next) {
this.next = next;
}
}
注:环形链表的添加都是往远离头结点的地方添加,即新加的节点的next域都是frist(头节点)
3.环形链表的节点的出圈顺序
注意:m-1次是因为m是移动次数,但是小孩报数,自身也要报一次,所以比如喊的是2,其实移动就是一次,即m-1
/**
* 节点出圈的方法
* startno-->从第几个结点开始的出圈
* countnum-->每次数几个数
* nums-->一开始有多少个节点
*/
public void countboy(int startno,int countnum,int nums){
//1.先进行数据检验
if(frist == null || startno<1 || startno>nums){
System.out.println("输入数据有误");
return;
}
//2.创造helper指针
Boy1 helper = frist;
//3.现在将指针指到尾部去
while (true) {
if(helper.getNext() == frist){
break;
}
helper = helper.getNext();
}
//4.小孩报数前,先让 first 和 helper 移动 k - 1次,移动到k节点
for (int i = 0; i < startno - 1; i++) {
helper = helper.getNext();
frist = frist.getNext();
}
//5.出圈
while(true){
if(helper == frist){//此时表明只剩一个节点
break;
}
for (int i = 0; i < countnum - 1; i++) {
frist = frist.getNext();
helper = helper.getNext();
}
System.out.printf("小孩%d出圈\n", frist.getNo());
frist = frist.getNext();
helper.setNext(frist);
}
System.out.printf("最后留在圈中的小孩编号%d \n", frist.getNo());
}
第三章 栈
一、概念
栈(stack)是限制线性表中元素的插入和删除只能在线性表的同一端进行的一种特殊线性表。允许插入和删除的一端,为变化的一端,称为栈顶(Top),另一端为固定的一端,称为栈底(Bottom)
1.特点
①先入后出的有序列表
②栈底不变,先取栈顶,栈顶下移
③栈的本质也是数组
2.应用场景
①子程序的调用:在跳往子程序前,会先将下个指令的地址存到堆栈中,直到子程序执行完后再将地址取出,以回到原来的程序中。
②处理递归调用:和子程序的调用类似,只是除了储存下一个指令的地址外,也将参数、区域变量等数据存入堆栈中。
③ 表达式的转换[中缀表达式转后缀表达式]与求值(实际解决)。
④二叉树的遍历。
⑤图形的深度优先(depth 一 first)搜索法。
3.栈的思路分析
注:当有数据入栈时,会先执行栈顶指针自加。初始化为空栈,规定空栈的时候指向-1,这样在有第一个元素入栈的时候栈顶指针就可以通过自加指向0元素,从而避免其它判断。
二、栈的构建与应用
1.栈的构建
构建一个栈类,本质是构建一个带有指针的数组,还需要一些自带的方法,判断栈满、判断栈空、出入栈操作以及遍历栈。
class Stack{
private int maxsize;
private int[] stack;
private int top = -1;
public Stack(int maxsize){
this.maxsize = maxsize;
stack = new int[maxsize];
}
//判断栈满
public boolean isFull(){return top == maxsize - 1;}
//判断栈空
public boolean isEmpty(){return top == -1;}
//出栈(弹栈)
public int pop(){
if(isEmpty()){
throw new RuntimeException("栈空,无法出栈");
}
int value = stack[top--];
return value;
}
//入栈(压栈)
public void push(int value){
if(isFull()){
throw new RuntimeException("栈满,无法入栈");
}
stack[++top] = value;
}
//遍历栈(从栈顶开始显示数据)
public void list(){
if(isEmpty()){
throw new RuntimeException("栈空,无法遍历");
}
for (int i = top; i >= 0; i--) {
System.out.printf("stack[%d]=%d\n", i, stack[i]);
}
}
}
注:
- 入栈的时候先移动top指针,再在top指针处添加,所以是++top
- 出栈的时候先把出栈的数保存一下,再下移top指针,所以是top–
- 由于栈的存入是自底向上的,所以出栈的顺序由栈顶开始,所以遍历的时候要从上到下进行遍历
三、前中后缀表达式
1.概念
- 前缀(波兰)表达式:从右至左扫描表达式,遇到数字时,将数字压入堆栈,遇到运算符时,弹出栈顶的两个数,用运算符对他们进行相应的运算(栈顶与次栈顶),并将结果入栈,重复上述过程直到最左端,最后的值即为结果。
- 中缀表达式:也就是以人的角度计算表达式,一般计算机不好操作,转为后缀表达式
- 后缀(逆波兰)表达式:从左至右扫描表达式,遇到数字时,将数字压入堆栈,遇到运算符时,弹出栈顶的两个数,用运算符对他们进行相应的运算(栈顶与次栈顶),并将结果入栈,重复上述过程直到最右端,最后的值即为结果。
四、中缀表达式转为后缀表达式
步骤:
- 初始化两个栈,运算符栈s1和储存中间结果的栈s2
- 从左至右扫描中缀表达式
- 遇到操作数时,将其压s2
- 遇到运算符时,比较其与s1栈顶运算符的优先级:
①如果s1为空,或栈顶运算符为左括号“(”,则直接将此运算符入栈
②否则,若优先级比栈顶运算符的高,也将运算符压入s1
③否则,将s1栈顶的运算符弹出并压入到s2中,再转到①与s1中新的栈顶运算符相比较 - 遇到括号时:
①如果是左括号“(”,直接压入s1
②如果是右括号“)”,依次弹出s1栈顶的运算符,并压入s2,直到遇到左括号为止,此时将这一对括号丢弃 - 重复步骤2-5,直到表达式的最右边
- 将s1中剩余的运算符依次弹出并压入s2
- 依次弹出s2中的元素并输出,结果的逆序即为中缀表达式对应的后缀表达式
//将得到的中缀表达式对应的List => 后缀表达式对应的List
public static List<String> parseSuffixExpreesionList(List<String> ls) {
//定义两个栈
Stack<String> s1 = new Stack<String>(); // 符号栈
//说明:因为s2 这个栈,在整个转换过程中,没有pop操作,而且后面我们还需要逆序输出
//因此比较麻烦,这里我们就不用 Stack<String> 直接使用 List<String> s2
//Stack<String> s2 = new Stack<String>(); // 储存中间结果的栈s2
List<String> s2 = new ArrayList<String>(); // 储存中间结果的Lists2
//遍历ls
for(String item: ls) {
//如果是一个数,加入s2
if(item.matches("\\d+")) {
s2.add(item);
} else if (item.equals("(")) {
s1.push(item);
} else if (item.equals(")")) {
//如果是右括号“)”,则依次弹出s1栈顶的运算符,并压入s2,直到遇到左括号为止,此时将这一对括号丢弃
while(!s1.peek().equals("(")) {
s2.add(s1.pop());
}
s1.pop();//!!! 将 ( 弹出 s1栈, 消除小括号
} else {
//当item的优先级小于等于s1栈顶运算符, 将s1栈顶的运算符弹出并加入到s2中,再次转到(4.1)与s1中新的栈顶运算符相比较
//问题:我们缺少一个比较优先级高低的方法
while(s1.size() != 0 && Operation.getValue(s1.peek()) >= Operation.getValue(item) ) {
s2.add(s1.pop());
}
//还需要将item压入栈
s1.push(item);
}
}
//将s1中剩余的运算符依次弹出并加入s2
while(s1.size() != 0) {
s2.add(s1.pop());
}
return s2; //注意因为是存放到List, 因此按顺序输出就是对应的后缀表达式对应的List
}
第四章 递归
一、概念
1.递归的理解
简单的说: 递归就是方法自己调用自己,每次调用时传入不同的变量.递归有助于编程者解决复杂的问题,同时可以让代码变得简洁。
2.递归调用规则
①当程序执行到一个方法时,就会开辟一个独立的空间(栈)
②每个空间的数据(局部变量),是独立的
3.递归能解决的问题
- 各种数学问题如: 8 皇后问题 , 汉诺塔, 阶乘问题, 迷宫问题, 球和篮子的问题(google 编程大赛)
- 各种算法中也会使用到递归,比如快排,归并排序,二分查找,分治算法等.
- 将用栈解决的问题–>递归代码比较简洁
4.递归需要遵守的重要规则
- 执行一个方法时,就创建一个新的受保护的独立空间(栈空间)
- 方法的局部变量是独立的,不会相互影响, 比如 n 变量
- 如果方法中使用的是引用类型变量(比如数组),就会共享该引用类型的数据.
- 递归必须向退出递归的条件逼近,否则就是无限递归,出现
StackOverflowError
,死龟了:) - 当一个方法执行完毕,或者遇到 return,就会返回,遵守谁调用,就将结果返回给谁,同时当方法执行完毕或者返回时,该方法也就执行完毕
5.迷宫问题体现回溯
public class MiGongTest {
public static void main(String[] args) {
int[][] map = new int[8][7];
//上下设置为1,设置墙壁
for (int i = 0; i < 7; i++) {
map[0][i] = 1;
map[7][i] = 1;
}
// 左右全部置为1
for (int i = 0; i < 8; i++) {
map[i][0] = 1;
map[i][6] = 1;
}
//设置挡板
map[3][1] = 1;
map[3][2] = 1;
//遍历即可,即可观察到标记过的迷宫
setWay(map,1,1);
System.out.println("小球走过,并标识过的 地图的情况");
for (int i = 0; i < 8; i++) {
for (int j = 0; j < 7; j++) {
System.out.print(map[i][j] + " ");
}
System.out.println();
}
}
public static boolean setWay(int[][] map,int i,int j){
if(map[6][5] == 2){//如果最后这点可以走到,就返回2,表明通路可以走
return true;
}else {//如果这点还没走到
if(map[i][j] == 0){//如果此时的点还没走过
map[i][j] = 2;//这点置为2,表明该点已经走过了
if(setWay(map,i+1,j)){//下
return true;
}else if(setWay(map,i,j+1)){//右
return true;
}else if(setWay(map,i-1,j)){//上
return true;
}else if(setWay(map,i,i-1)){//左
return true;
}else {
map[i][j] = 3;//非终点的点上下左右都走不通了,所以就死了,返回false
return false;
}
}else {//此时的点不是0,是123,非终点的点是123,拿着通路就没走成,返回false
return false;
}
}
}
}
第五章 排序
一、排序概念
排序也称排序算法(Sort Algorithm),排序是将一组数据,依指定的顺序进行排列的过程。
1.排序的分类
- 内部排序:
指将需要处理的所有数据都加载到内部存储器(内存)中进行排序。 - 外部排序:
数据量过大,无法全部加载到内存中,需要借助外部存储(文件等)进行排序 - 常见排序:
2.算法的复杂度
2.1时间复杂度的度量方法
①事后统计:(受硬件、软件环境影响且须程序运行后统计),要在同一计算机的相同状态下运行
②事前估算:通过分析某个算法的时间复杂度来判断哪个算法更优。
2.2时间频度
一个算法花费的时间与算法中语句的执行次数成正比例,哪个算法中语句执行次数多,它花费时间
就多。一个算法中的语句执行次数称为语句频度或时间频度。记为 T(n)。
- 随着n变大,可忽略常数项
- 随着n的幂次变大,可忽略低次项
- 随着n的幂次变大,可忽略系数
2.3时间复杂度
一个算法执行所耗费的时间
①一般情况下,算法中的基本操作语句的重复执行次数是问题规模n的某个函数,用T(n)表示。T(n)/f(n)=1,T(n)与f(n)同量级。记作T(n)=o(f(n))称o(f(n))为算法的渐进时间复杂度
②T(n)不同,但时间复杂度不同
③事件复杂度的计算:
- 用常数1代替运行时间中所有加法常数T(n)=2n²+7n+6 => T(n)=2n²+7n+1
- 修改后的运行次数函数中,只保留最高阶项 T(n)=2n²+7n+1 => T(n) = 2n²
- 去除最高阶项的系数 T(n) = n² => O( n² )
2.4常见时间复杂度
①常数阶 O(1) —> 只有单行代码,无循环复杂结构
②对数阶 O(log2n) —> 某循环内变量遵循倍数增长
③线性阶 O(n) —> 单层循环
④线性对数阶 O(nlog2n) —> 双层循环,其中内部循环变量遵循倍数增长
⑤平方阶 O(n^2) —> 双层循环
⑥立方阶 O(n^3) —> 三层循环
⑦ k 次方阶 O(n^k) —> k层循环
⑧ 指数阶 O(2^n)
常见的算法时间复杂度由小到大依次为:Ο(1)<Ο(log2n)<Ο(n)<Ο(nlog2n)<Ο(n^2) <Ο(n^3)< Ο(n^k) <Ο(2n) ,随着问题规模 n 的不断增大,上述时间复杂度不断增大,算法的执行效率越低,我们应该尽可能避免使用指数阶的算法。
2.5平均时间复杂度
平均时间复杂度是指所有可能的输入实例均以等概率出现的情况下,该算法的运行时间。
2.6最坏时间复杂度
最坏情况下的时间复杂度称最坏时间复杂度。一般讨论的时间复杂度均是最坏情况下的时间复杂度。这样做的原因是:最坏情况下的时间复杂度是算法在任何输入实例上运行时间的界限,这就保证了算法的运行时间不会比最坏情况更长。
2.7空间复杂度
运行完一个程序所需内存的大小
- 类似于时间复杂度的讨论,一个算法的空间复杂度(Space Complexity)定义为该算法所耗费的存储空间,它也是问题规模 n 的函数。
- 空间复杂度(Space Complexity)是对一个算法在运行过程中临时占用存储空间大小的量度。有的算法需要占用的临时工作单元数与解决问题的规模 n 有关,它随着 n 的增大而增大,当 n 较大时,将占用较多的存储单元,例如快速排序和归并排序算法, 基数排序就属于这种情况。
- 在做算法分析时,主要讨论的是时间复杂度。从用户使用体验上看,更看重的程序执行的速度。一些缓存产品(redis, memcache)和算法(基数排序)本质就是用空间换时间
二、冒泡排序
public static void bubble(int []arr){
boolean flag = false;
int temp = 0;
for (int i = 0; i < arr.length - 1; i++) {
for (int j = 0; j < arr.length - 1 - i; j++) {
if (arr[j] > arr[j + 1]) {
flag = true;
temp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = temp;
}
}
if(!flag){
break;
}else{
flag = false;
}
}
}
理解:
- 外层循环是冒泡排序要经历数组长度-1次遍历,而内层循环指的是指针从新的位置开始遍历,判断当前数和下一个数的大小
- 两个数进行比较,如果后数大于前数,加通过中间数(temp)进行交换
- 如果没有进行交换就说明,数组是按顺序排列的,此时直接跳出内层循环,即开始新的位置遍历
三、选择排序
public static void selectSort1(int[] arr){
int minIndex = 0;
for (int i = 0; i < arr.length - 1; i++) {
minIndex = i;//先假設最小索引为i
for (int j = i + 1; j < arr.length; j++) {
if (arr[j] < arr[minIndex]) {
minIndex = j;//遍历后找到最小数的索引值
}
}
// 交换元素
int temp = arr[i];
arr[i] = arr[minIndex];
arr[minIndex] = temp;
}
}
理解:
- 先找出数组中最小的,拿最小的和第一个值比较,小的放到最前面
四、插入排序
public static void InsertSort1(int[] arr) {
int tmp;//中间数
int i;
int j;
for (i = 1; i < arr.length; i++) {//数组要遍历数组长度-1次
tmp = arr[i];//保存一下后面的数
for (j = i - 1; j >= 0 && arr[j] > tmp; j--) {//判断前面的每个数
arr[j + 1] = arr[j];//如果前面的大于后面的就把前面的数放到后面
}
arr[j + 1] = tmp;//后面的数交换成为前面的数
}
}
五、希尔排序
希尔排序有两种:对有序序列在插入时采用交换法,对有序序列在插入时采用移位法
1.交换法
public static void shellSort(int[] arr) {
int temp = 0;
int count = 0;
// 根据前面的逐步分析,使用循环处理
for (int gap = arr.length / 2; gap > 0; gap /= 2) {
for (int i = gap; i < arr.length; i++) {
// 遍历各组中所有的元素(共gap组,每组有个元素), 步长gap
for (int j = i - gap; j >= 0; j -= gap) {
// 如果当前元素大于加上步长后的那个元素,说明交换
if (arr[j] > arr[j + gap]) {
temp = arr[j];
arr[j] = arr[j + gap];
arr[j + gap] = temp;
}
}
}
}
}
2.移位法
public static void ShellSort(int[] arr){
for (int gap = arr.length/2 ; gap>0 ; gap/=2){//先分大组,分好后进行遍历
for (int i = gap; i < arr.length; i++) {//分小组,遍历小组
int j = i;//小组当前(大数)索引给i
int temp = arr[j];//小组当前数(大数)给temp
if(arr[j]<arr[j-gap]){//如果小组内的小数大于大数
while(j - gap >= 0 && temp<arr[j-gap]){//进行移动
//移动
arr[j] = arr[j-gap];//小数给大数
j-=gap;//j变为小数索引
}
arr[j] = temp;//大数给小数
}
}
}
}
六、快速排序
public static void quicksort(int arr[], int left, int right) {
int l = left;//左指针
int r = right;//右指针
int pivot = arr[(left + right) / 2];//基准值
int temp = 0;
while (l < r) {
while (arr[l] < pivot) {//左指针对应数小于基准值
l += 1;//左指针右移
}
while (pivot < arr[r]) {//右指针对应数大于基准值
r -= 1;//右指针左移
}
if(l>=r){//如果移着移着,左指针大于等于右指针,直接结束
break;
}
temp = arr[r];//左右指针指针移动完之后,进行数据交换
arr[r] = arr[l];
arr[l] = temp;
if (arr[r] == pivot) {//如果此时的数和基准值相同
l += 1;
}
if (arr[l] == pivot) {
r -= 1;
}
}
// 如果 l == r, 必须l++, r--, 否则为出现栈溢出
if (l == r) {
r -= 1;
l += 1;
}
//向右递归
if (right > l) {
quicksort(arr, l, right);
}
//向左递归
if (r > left) {
quicksort(arr, left, r);
}
}
七、归并排序
该排序是利用归并的思想实现的排序方法,采用经典的分治策略。
分治法将一些问题分成一些小的问题然后递归求解,而治的阶段则将分的阶段得到的答案修补在一起
归并排序的实现分两步:
1.首先从中间拆成两个数组,同时建立一个新数组,比较两数组中元素小的那个,放到新数组中
public static void merge(int[] arr, int left, int mid, int right, int[] temp) {
int i = left; // 初始化i, 左边有序序列的初始索引
int j = mid + 1; //初始化j, 右边有序序列的初始索引
int t = 0; // 指向temp数组的当前索引
//先把左右两边(有序)的数据按照规则填充到temp数组
//直到左右两边的有序序列,有一边处理完毕为止
while (i <= mid && j <= right) {
if (arr[i] <= arr[j]) {
temp[t] = arr[i];
t += 1;
i += 1;
} else {//反之,将右边有序序列的当前元素,填充到temp数组
temp[t] = arr[j];
t += 1;
j += 1;
}
}
//把有剩余数据的一边的数据依次全部填充到temp
while (i <= mid) {
temp[t] = arr[i];
t += 1;
i += 1;
}
while (j <= right) {
temp[t] = arr[j];
t += 1;
j += 1;
}
//将temp数组的元素拷贝到arr
//注意,并不是每次都拷贝所有
t=0;
int templeft=left;
while(templeft<=right){
arr[templeft]=temp[t];
t+=1;
templeft+=1;
}
}
2.其次将数组进行合并
public static void mergesort(int []arr,int left,int right,int temp[]){
if(left < right){
int mid=(left+right)/2;
mergesort(arr, left, mid, temp);//向左分
mergesort(arr, mid+1, right, temp);//向右分,到这一步只是将数组分开了,没有合
merge(arr, left, mid, right, temp);//合并
}
}
八、基数排序
1.基数排序介绍
- 基数排序(radix sort)属于“分配式排序”(distribution sort),又称“桶子法”(bucket sort)或 (bin sort),顾名思义,它是通过键值的各个位的值,将要排序的元素分配至某些“桶”中,达到排序的作用
- 基数排序法是属于稳定性的排序,基数排序法的是效率高的稳定性排序法
- 基数排序(Radix Sort)是桶排序的扩展
- 基数排序实现:将整数按位数切割成不同的数字,然后按每个位数分别比较。
2.图文实现
初始化十个桶,将个十百位数,分别装入比较
3.代码
public static void radixSort(int[] arr){
//1.取得最大数
int max = arr[0]; //假设最大数是arr[0]
//2.遍历找最大数
for (int i = 1; i < arr.length; i++) {
if(arr[i]>max){
max = arr[i];
}
}
//已经找到最大数,规定最大数的长度
int maxlength = (max + "").length();
//定义桶和桶中的数
int[][] bucket = new int[10][arr.length];
int[] bucketElementsCounts = new int[10];
//3.为了记录每个桶中,实际存放了多少个数据,我们定义一个一维数组来记录各个桶的每次放入的数据个数
for (int i = 0,n = 1; i < maxlength; i++,n*=10) {//遍历每个桶, 第一次是个位,第二次是十位,第三次是百位..
for (int j = 0; j < arr.length; j++) {//遍历每个桶中的数,并装入
int digitOfElement = arr[j] / n % 10;//取得他的个位数
bucket[digitOfElement][bucketElementsCounts[digitOfElement]] = arr[j];//放入到对应的桶中
bucketElementsCounts[digitOfElement]++;//桶中的数++
}
//按照这个桶的顺序(一维数组的下标依次取出数据,放入原来数组)
int index = 0;
for (int x = 0; x < bucketElementsCounts.length; x++) {//遍历每个桶中的数,并取出装入新数组
//如果桶中,有数据,我们才放入到原数组
if(bucketElementsCounts[x] != 0){
//循环该桶即第k个桶(即第k个一维数组), 放入
for (int k = 0; k < bucketElementsCounts[x]; k++) {//遍历每个桶中的数
//取出元素放入到arr
arr[index++] = bucket[x][k];
}
}
//第i+1轮处理后,需要将每个 bucketElementCounts[x] = 0 !!!!即清空桶
bucketElementsCounts[x] = 0;
}
}
}
九、堆排序
1.概念
- 堆排序是利用堆这种数据结构而设计的一种排序算法,堆排序是一种选择排序,它的最坏,最好,平均时间复杂度均为 O(nlogn),它是不稳定排序。
- 堆是具有以下性质的完全二叉树:
- 每个结点的值都大于或等于其左右孩子结点的值,称为大顶堆
- 每个结点的值都小于或等于其左右孩子结点的值,称为小顶堆
- 一般升序采用大顶堆,降序采用小顶堆
- 大顶堆特点:
arr[i] >= arr[2i+1] && arr[i] >= arr[2i+2];
- 小顶堆特点:
arr[i] <= arr[2i+1] && arr[i] <= arr[2i+2];
2.堆排序基本思想
①将待排序序列构造成一个大顶堆
②此时,整个序列的最大值就是堆顶的根节点
③将其与末尾元素进行交换,此时末尾就为最大值
④然后将剩余 n-1 个元素重新构造成一个堆,这样会得到 n 个元素的次小值。如此反复执行,便能得到一个有序序列了
3.代码思路
①将无序序列构建成一个堆,根据升序降序需求选择大顶堆或小顶堆
②将堆顶元素与末尾元素交换,将最大元素"沉"到数组末端
③重新调整结构,使其满足堆定义,然后继续交换堆顶元素与当前末尾元素,反复执行调整+交换步骤,直到整个序列有序
4.代码实现
分为两大步:
首先是大顶堆的构建:
public static void adjustheap(int[] arr, int i,int length){
int temp = arr[i];//先取出当前元素的值,保存在临时变量
for (int k = 2*i + 1; k < length; k=2*k+1) {//k = i * 2 + 1 k 是 i节点的左子节点
if(k+1 < length && arr[k] < arr[k+1]){//如果左子节点小于右子节点,就把指针指到右子节点上
k++;//即k++
}
if(temp < arr[k]){//此时k在右子节点上,如果右子节点的数大于当前的节点的数
arr[i] = arr[k];//就把右子节点(大数)给到子树根(也就是一开始的当前节点,小数)
i = k;//指针i此时指在右子节点上
}else{
break;//如果子节点比根节点小,就不管,不操作
}
arr[i] = temp;//此时将小数给右子节点,完成互换
}
}
其次是进行堆排序:
/**
* 功能:完成将以i对应的非叶子结点的树调整成大顶堆
* @param arr 待调整的数组
* @param i 表示非叶子结点在数组中索引
* @param length 表示对多少个元素继续调整, length是在逐渐的减少
*/
public static void heapsort(int[] arr){
int temp = 0;
for (int i = arr.length / 2 - 1; i >= 0; i--) {//大顶堆的构建要经历(非叶子节点的个数)次
adjustheap(arr,i,arr.length);
}
for (int i = arr.length - 1; i > 0; i--) {//一共排序要经历(数组长度-1)次
temp = arr[i];
arr[i] = arr[0];
arr[0] = temp;//此时把大顶堆上的大数和数组的第一个(最小的数)互换
adjustheap(arr,0,i);
}
}
十、常用算法比较
- 稳定:如果 a 原本在 b 前面,而 a=b,排序之后 a 仍然在 b 的前面;
- 不稳定:如果 a 原本在 b 的前面,而 a=b,排序之后 a 可能会出现在 b 的后面;
- n: 数据规模
- “桶”的个数
- In-place: 不占用额外内存
- Out-place: 占用额外内存
第六章 查找算法
一、种类
通常用的查找算法有四种:顺序(线性)查找,二分(折半)查找,插值查找,斐波那契查找
二、线性查找
较为简单,遍历查找即可,查找效率不高
三、二分查找
1.代码思路
①定义两个辅助指针:left和right,待查找元素在arr[left]–arr[right]之间
②left初始值为0,right初始值为arr.length-1
③将数组分成两半:int mid = (left+right)/2
取中间数与目标数findval比较:
- 若mid>findval,说明查找的数在左半部分,向左递归
- 若mid<findval,说明查找的数在右半部分,向右递归
- 若mid=findval,查找到目标值,返回即可
2.终止递归
①找到目标值,直接返回目标值findval,结束递归即可
②未找到目标值:left>right,左索引比右索引大,数已经被查找完,返回-1即可
3.代码实现
public static int bsearch2(int[] arr,int left,int right,int findval){
if(left>right){
return -1;
}
int mid = (left+right)/2;
int midval = arr[mid];
if(midval>findval){//查询数小于中间值
return bsearch2(arr,left,mid-1,findval);//向左递归
}else if(midval<findval){//查询数大于中间值
return bsearch2(arr,mid+1,right,findval);//向右递归
}else {
return mid;
}
}
四、插值查找
1.特点
①大致思路与二分查找一样,只是寻找mid公式不同
②对于数据量较大,关键字分布较均匀(最好是线性分布)的查找表,采用插值查找速度较快
③关键字分布不均匀的情况下,该方法不一定比二分查找好
2.代码实现
public static int InsertSearch(int[] arr,int left,int right,int findval){
if(left>right){
return -1;
}
int mid = left+(right-left)*(findval-arr[left])/(arr[right]-arr[left]);
int midval = arr[mid];
if(midval>findval){//查询数小于中间值
return bsearch2(arr,left,mid-1,findval);//向左递归
}else if(midval<findval){//查询数大于中间值
return bsearch2(arr,mid+1,right,findval);//向右递归
}else {
return mid;
}
}
五、*斐波那契(黄金分割法)查找
1.代码实现
public class FibonacciSearch {
public static int maxSize = 20;
public static void main(String[] args) {
int [] arr = {1,8, 10, 89, 1000, 1234};
System.out.println("index=" + fibSearch(arr, 189));// 0
}
//因为后面我们mid=low+F(k-1)-1,需要使用到斐波那契数列,因此我们需要先获取到一个斐波那契数列
//非递归方法得到一个斐波那契数列
public static int[] fib() {
int[] f = new int[maxSize];
f[0] = 1;
f[1] = 1;
for (int i = 2; i < maxSize; i++) {
f[i] = f[i - 1] + f[i - 2];
}
return f;
}
//编写斐波那契查找算法
//使用非递归的方式编写算法
/**
*
* @param a 数组
* @param key 我们需要查找的关键码(值)
* @return 返回对应的下标,如果没有-1
*/
public static int fibSearch(int[] a, int key) {
int low = 0;
int high = a.length - 1;
int k = 0; //表示斐波那契分割数值的下标
int mid = 0; //存放mid值
int f[] = fib(); //获取到斐波那契数列
//获取到斐波那契分割数值的下标
while(high > f[k] - 1) {
k++;
}
//因为 f[k] 值 可能大于 a 的 长度,因此我们需要使用Arrays类,构造一个新的数组,并指向temp[]
//不足的部分会使用0填充
int[] temp = Arrays.copyOf(a, f[k]);
//实际上需求使用a数组最后的数填充 temp
//举例:
//temp = {1,8, 10, 89, 1000, 1234, 0, 0} => {1,8, 10, 89, 1000, 1234, 1234, 1234,}
for(int i = high + 1; i < temp.length; i++) {
temp[i] = a[high];
}
// 使用while来循环处理,找到我们的数 key
while (low <= high) { // 只要这个条件满足,就可以找
mid = low + f[k - 1] - 1;
if(key < temp[mid]) { //我们应该继续向数组的前面查找(左边)
high = mid - 1;
//为甚是 k--
//说明
//1. 全部元素 = 前面的元素 + 后边元素
//2. f[k] = f[k-1] + f[k-2]
//因为 前面有 f[k-1]个元素,所以可以继续拆分 f[k-1] = f[k-2] + f[k-3]
//即 在 f[k-1] 的前面继续查找 k--
//即下次循环 mid = f[k-1-1]-1
k--;
} else if ( key > temp[mid]) { // 我们应该继续向数组的后面查找(右边)
low = mid + 1;
//为什么是k -=2
//说明
//1. 全部元素 = 前面的元素 + 后边元素
//2. f[k] = f[k-1] + f[k-2]
//3. 因为后面我们有f[k-2] 所以可以继续拆分 f[k-1] = f[k-3] + f[k-4]
//4. 即在f[k-2] 的前面进行查找 k -=2
//5. 即下次循环 mid = f[k - 1 - 2] - 1
k -= 2;
} else { //找到
//需要确定,返回的是哪个下标
if(mid <= high) {
return mid;
} else {
return high;
}
}
}
return -1;
}
}
第七章 哈希(散列)表
一、概述
散列表(Hash table,也叫哈希表),是根据关键码值(Key value)而直接进行访问的数据结构。也就是说,它通过把关键码值映射到表中一个位置来访问记录,以加快查找的速度。这个映射函数叫做散列函数,存放记录的数组叫做散列表。
二、哈希表构造
1.构造节点
class Emp {
public int id;
public String name;
public Emp next; //next 默认为 null
public Emp(int id, String name) {
super();
this.id = id;
this.name = name;
}
}
2.构造单链表
class EmpLinkedList{//单链表
private employ head;
public void add(employ emp){
if(head == null){
head = emp;
return;
}
employ curemp = head;
while (true) {
if(curemp.next == null){
break;
}
curemp = curemp.next;
}
curemp.next = emp;
}
public void list(int no){
if(head == null){
System.out.println("第 "+(no+1)+" 链表为空");
return;
}
System.out.print("第 "+(no+1)+"链表的信息为");
employ curemp = head;
while (true) {
System.out.printf(" => id=%d name=%s\t", curemp.id, curemp.name);
if(curemp.next == null){
break;
}
curemp = curemp.next;
}
System.out.println();
}
public employ findEmpById(int id){
if(head == null){
System.out.println("链表为空");
return null;
}
employ curemp = head;
while (true) {
if(curemp.id == id){
break;
}
if(curemp.next == null){
curemp = null;
break;
}
curemp = curemp.next;
}
return curemp;
}
3.构造Hash表
class hashtab{
private EmpLinkedList[] empLinkedListArray;//需要一组单链表
private int size;//单链表的大小
public hashtab(int size){//哈希表初始化
this.size=size;
empLinkedListArray=new EmpLinkedList[size];
for(int i=0;i<empLinkedListArray.length;i++){//遍历数组每个元素都挂个单链表
empLinkedListArray[i]= new EmpLinkedList();
}
}
void add(employ emp){//添加节点
int empLinkedListNO = hashFun(emp.id);//根据散列函数找到要添加节点的单链表号
empLinkedListArray[empLinkedListNO].add(emp);//根据empLinkedListNO号的链表加入emp
}
public int hashFun(int id) {
return id % size;
}//散列函数
public void list() {
for(int i = 0; i < size; i++) {
empLinkedListArray[i].list(i);//遍历第i个大组的每个emp对象
}
}
public void findEmpById(int id) {
//使用散列函数确定到哪条链表查找
int empLinkedListNO = hashFun(id);
employ emp = empLinkedListArray[empLinkedListNO].findEmpById(id);
if(emp != null) {//找到
System.out.printf("在第%d条链表中找到 雇员 id = %d\n", (empLinkedListNO + 1), id);
}else{
System.out.println("在哈希表中,没有找到该雇员~");
}
}
}
三、哈希表应用
public class HashTableDemo {
public static void main(String[] args) {
hashtab h = new hashtab(7);
String key="";
Scanner scanner = new Scanner(System.in);
while(true){
System.out.println("add 添加雇员:");
System.out.println("exit 退出系统!");
System.out.println("list 显示雇员:");
System.out.println("find 查找雇员:");
key=scanner.next();
switch (key) {
case "add":
System.out.println("输入id:");
int id=scanner.nextInt();
System.out.println("输入名字:");
String name=scanner.next();
employ emp=new employ(id, name);
h.add(emp);
break;
case "list":
h.list();
break;
case "find":
System.out.println("请输入要查找的id");
id = scanner.nextInt();
h.findEmpById(id);
break;
case "exit":
scanner.close();
System.exit(0);
default:
break;
}
}
}
}