程序 = 数据结构 + 算法
算法特性:输入、输出、有穷性、确定性、可靠性
算法目标:正确性、可读性、健壮性、运行时间少、内存空间小
时间复杂度是用来估计算法运行时间的一个单位。
一般来说,时间复杂度高的算法比时间复杂度低的算法慢。
常见时间复杂度(按效率排序):
O(1) < O(logn) < O(n) < O(nlogn) < O() < O(
logn) < O(
)
复杂问题的时间复杂度:
O(n!) < O() < O(
)
如何快速判断算法复杂度(适用绝大部分简单情况):
空间复杂度是用来评估算法内存占用大小的单位。
空间复杂度的表示方式与时间复杂度完全一样:
空间换时间
递归的两个特点:调用自身、结束条件
大梵天创建世界的时候做了三根金刚石柱子,在一根柱子上从下往上按照大小顺序摞着64片黄金圆盘;大梵天命令婆罗门把圆盘从下面开始按照大小顺序重新摆放在另一个柱子上;规则是:在小圆盘上不能放大圆盘,在三根柱子之间一次只能移动一个圆盘;64个圆盘移动完毕之日,就是世界毁灭之时。
1个盘子:需要1步,2个盘子:需要3步,3个盘子:需要7步;那么 64个盘子:需要 -1=18,446,744,073,709,551,615步
假设移动一次需要1s,那么移动 -1 次大概需要 5849亿年
public class HanoiTower {public static void main(String[] args) {function(3, 'A', 'B', 'C');}public static void function(int n, char a, char b, char c) {if (n > 0) {function(n-1, a, c, b);System.out.println("Moving " + a + " to " + c);function(n-1, b, a, c);}}
}
查找:在一些数据元素中,通过一定的方法找出与给定关键字相同的数据元素的过程。
列表查找(线性表查找):从列表中查找指定元素; 输入:列表、待查找元素; 输出:元素下标(未找到元素一般返回None或-1);
也叫线性查找,从列表第一个元素开始,顺序进行搜索,直到找到元素或搜索到列表最后一个元素为止。内置函数indexOf()。
循环减半查找,时间复杂度是O(logn),但是前提是必须先对列表元素进行排序,排序的时间复杂度 > O(n)。
public class BinarySearch {public static void main(String[] args) {int[] list = new int[]{1,2,3,4,5,6,7,8,9};System.out.println(function(list, 3));}public static int function(int[] list, int val) {int left = 0;int right = list.length - 1;int mid = 0;while (left <= right) {//说明候选区还有值mid = (left + right) / 2;if (list[mid] == val) {return mid;} else if (mid > val) {//待查找的值在mid左侧right = mid - 1;} else if (mid < val) {//待查找的值在mid右侧left = mid + 1;}}return -1;}
}
将一组无序的列表变成有序的列表;输入:列表;输出:有序列表;有升序与降序两种排序方式;内置排序函数sort()。
列表每两个相邻的数,如果前面的比后面的大,则交换这两个数;一次排序后,则无序区少一个元素,有序区多一个元素;
代码关键点:趟、无序区范围
冒泡排序优化:如果冒泡排序中的一轮排序没有发生任何位置交换,则说明当前列表已经有序,可以直接结束算法,不需要再循环排序。
public class BubbleSort {public static void main(String[] args) {int[] list = new int[]{2,5,7,36,6,7,3,33,24,8,9};System.out.println(Arrays.toString(function(list)));}public static int[] function(int[] list) {int temp;// 外层循环控制比较的轮数for (int i = 0; i < list.length-1; i++) {// 内层循环控制每轮比较的次数boolean exchange = false; // 冒泡排序优化:如果前面几轮已经对整个列表排好顺序,后面不需要再次循环for (int j = 0; j < list.length-i-1; j++) {if (list[j] > list[j+1]) {//如果前面的数比后面的数大,互换位置temp = list[j];list[j] = list[j+1];list[j+1] = temp;exchange = true; // 冒泡排序优化:如果前面几轮已经对整个列表排好顺序,后面不需要再次循环}}System.out.println(Arrays.toString(list));if (!exchange) { // 冒泡排序优化:如果前面几轮已经对整个列表排好顺序,后面不需要再次循环return list;}}return list;}
}
一轮排序记录最小的数,放到第一个位置;再一轮排序记录无序区列表中最小的数,放到第二个位置。
算法关键点:有序区与无序区、无序区最小数的位置;
public class SelectSort {public static void main(String[] args) {int[] list = new int[]{8,4,2,3,7,1,6,9,5};function(list);}// 简单选择排序: 循环每次拿到最小的数,然后放在一个新的列表里// 缺点:时间复杂度O(n*n),空间复杂度O(2n),所以不推荐使用// 优化选择排序:循环每次拿到最小的数,和无序区第一个数交换位置,避免创建新的空间public static void function(int[] list) {// 外层循环控制比较的轮数int temp;for (int i = 0; i < list.length-1; i++) {// 内层循环控制每轮比较的次数int min_index = i;for (int j = i+1; j < list.length; j++) {if (list[j] < list[min_index]) {min_index = j;}}temp = list[i];list[i] = list[min_index];list[min_index] = temp;System.out.println(Arrays.toString(list)); //打印每次交换的过程}//System.out.println(Arrays.toString(list));//输出最终排序结果}
}
初始时手里(有序区)只有一张牌(一个元素),每次摸一张牌(从无序区取一个元素),插入到手里已有牌的正确位置;
public class InsertSort {public static void main(String[] args) {int[] list = new int[]{4,2,5,1,6,7,3,9,6,8};function(list);}public static void function(int[] list) {int temp, j;for (int i = 1; i < list.length; i++) {//摸到牌的下标temp = list[i];j = i - 1; //j指的是手里牌的下标while (j >= 0 && list[j] > temp) {//如果新摸的牌比手里的牌小,手里的牌向右移动list[j+1] = list[j];//手里的牌右移一个位置j -= 1;//比较的下标前移,下一次跟上一个位置的牌比较}list[j + 1] = temp;//比较完成后,把摸到的牌放在空出来的位置System.out.println(Arrays.toString(list));//输出每次插入的过程}//System.out.println(Arrays.toString(list));//输出算法结果}
}
快速排序思路:
(1) 取一个元素p(第一个元素),使元素p归位;(2) 列表被p分成两部分,左边都比p小,右边都比p大;(3) 递归完成排序。
public class QuickSort {public static void main(String[] args) {int[] list = new int[]{5,1,7,2,9,3,8,6,4};sort(list, 0, list.length-1);System.out.println(Arrays.toString(list));}// 快速排序算法public static void sort(int[] list, int left, int right) {if (left < right) {int mid = partition(list, left, right);//中间数左侧列表取第一个元素p并让其归位,分成两部分,左侧比p小,右侧比p大sort(list, left, mid - 1);//中间数右侧列表取第一个元素p并让其归位,分成两部分,左侧比p小,右侧比p大sort(list, mid + 1, right);}}// 让元素p归位,返回p所在位置索引public static int partition(int[] list, int left, int right) {int temp = list[left];while (left < right) {// 从右向左找比temp小的数while (left < right && list[right] >= temp) {right -= 1; //右边指针向左走一步}if (left < right) {list[left] = list[right];//把右边数移到左边空位上left++; //下一轮从左向右:减少一次比较的过程System.out.println(Arrays.toString(list));//打印右边数向p左边移动的过程}// 从左向右找比temp大的数while (left < right && list[left] <= temp) {left += 1; //左边指针向右走一步}if (left < right) {list[right] = list[left];//把左边数移到右边空位上right--; //下一轮从右向左:减少一次比较的过程System.out.println(Arrays.toString(list));//打印左边数向p右边移动的过程}}list[left] = temp;return left;}
}
时间复杂度:每一次归位的时间复杂度是O(n),需要做logn次归位,所以时间复杂度是O(nlogn);
快速排序的问题:递归消耗系统性能、最坏情况(列表元素正好倒序排列,时间复杂为 O()),针对最坏情况,可取随机位置数作为p;
树、二叉树、满二叉树、完全二叉树ssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssss | sssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssss |
---|---|
树:树是一种可以递归定义的数据结构,比如目录结构; 树是由n个节点组成的集合,1个节点作为根节点,其他节点可以分为m个集合,每个集合本身又是一个子树; 根节点:最顶上的节点; 叶子节点:最底层没有子节点的节点就是叶子节点; 树的深度:从根节点到叶子节点最多有多少层; 树的度:整个树中哪个节点分叉最多,分叉数量就是树的度; 二叉树:树中每个节点最多分两个叉,这种树就是二叉树; 满二叉树:每一层的节点数都达到了最大值,如右图(a); 完全二叉树:叶子节点只能出现在最下层和次下层,并且最下层的节点都集中在该层最左边的若干位置,如右图(b); | ![]() |
二叉树的存储方式:链式存储方式、顺序存储方式
二叉树的顺序存储方式 sssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssss | ssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssss |
---|---|
父节点和左子节点,下标之间有什么关系? 0-1,1-3,2-5,3-7,4-9 可以发现有一定规律:i --> 2i+1 父节点和右子节点,下标之间有什么关系? 0-2,1-4,2-6,3-8,4-10 可以发现有一定规律:i --> 2i+2 根据子节点下标找父节点下标:(i - 1) / 2取整,假设列表总长为n,那么最后一个子节点下标为 n-1,那么最后一个非叶子节点下标为 (n-2) / 2; | |
堆:一种特殊的完全二叉树结构
堆排序过程
public class HeapSort {public static void main(String[] args) {int[] list = new int[]{9,2,4,6,3,0,5,8,1,7};sort(list);}public static void sort(int[] list) {// 建堆:从下往上,从子堆向父堆开始构建for (int i = (list.length-2)/2; i > -1; i--) {sift(list, i, list.length-1);}System.out.println(Arrays.toString(list)); //打印建完堆后的列表// 挨个出数:把堆顶元素和最后一个元素互换位置,然后从堆顶到倒数第一二元素重新调整结构,变成大根堆,依次重复int temp, j;for (int i = list.length-1; i > -1; i--) {temp = list[0]; //把堆顶元素和堆尾元素互换list[0] = list[i];list[i] = temp;sift(list, 0, i-1); //i-1是新的堆尾}System.out.println(Arrays.toString(list)); //打印堆排序后的列表}// 向下调整:保证始终是一个大根堆(父节点元素值比子节点大)public static void sift(int[] list, int head, int tail) {int i = head; //父节点下标int j = 2 * i + 1; //左子节点下标int temp = list[i]; //把堆顶存起来while (j <= tail) { //避免下标越界if (j + 1 <= tail && list[j + 1] > list[j]) { //存在右子节点,且比左子节点大j = j + 1; //切换为右子节点}if (list[j] > temp) { //如果当前子节点比父节点大list[i] = list[j]; //将子节点元素存在父节点位置// 有可能下一层子节点元素仍然比temp元素大,所以需要继续向下比较,直到最后一个元素i = j; //父节点下移一层j = 2 * i + 1; //找到新的父节点对应的左子节点} else { //如果父节点元素temp更大,把temp就放在原位置,跳出循环break;}}list[i] = temp; //不存在子节点,或父节点元素temp比子节点元素大,将此元素放在原位置}
}
应用场景:top k问题
现在有 n 个数,设计算法得到前 k 个大的数(k < n)。
解决思路:
public class TopK {public static void main(String[] args) {int[] list = new int[]{9,2,4,8,6,3,7,1,5,10};sort(list, 3);}public static void sort(int[] list, int k) {// 1、建堆:取前k个元素建立小根堆int[] heap = new int[k];for (int i = 0; i < k; i++) {heap[i] = list[i];}// 将 k 个元素构建成小根堆for (int i = (k-2)/2; i > -1; i--) {siftdown(heap, i, k-1); // i是堆的最后一个非子节点}// 2、遍历列表后续元素,与小根堆堆顶元素比较for (int i = k; i < list.length-1; i++) {if (list[i] > heap[0]) { //如果列表后续元素比小根堆中最小的元素还大,那么替换掉堆顶最小元素heap[0] = list[i]; //替换堆顶最小元素siftdown(heap, 0, k-1); //重新对堆进行调整保证是小根堆}}// 3、小根堆挨个出数进行排序int temp;for (int i = k-1; i > -1; i--) {temp = heap[0];heap[0] = heap[i];heap[i] = temp;siftdown(heap, 0, i-1); //i-1是未排序的最后一个元素下标}System.out.println(Arrays.toString(heap));}// 向下调整:构建小根堆public static void siftdown(int[] list, int head, int tail) {int i = head; //父节点下标int j = 2 * i + 1; //左子节点int temp = list[i]; //把堆顶元素存起来while (j <= tail) { //避免下标越界if (j + 1 <= tail && list[j + 1] < list[j]) { //如果右子节点比左子节点小,切换到右子节点上j = j+ 1;}if (list[j] < temp) { //子节点比父节点小list[i] = list[j]; //将子节点元素存入父节点位置// 有可能还存在下级节点元素比temp小的情况,所以需要继续向下比较,直到最后一个元素i = j; //i下标移到下一级j = 2 * i + 1;} else { //如果父节点元素temp值更小,把temp存在原位置,跳出循环break;}}list[i] = temp; //不存在子节点,或父节点元素temp比子节点元素更小,将元素存在原位置}
}
归并:假设现在的列表分成两段,两段都有序,将其合成一个有序列表,这种操作就是一次归并;
归并过程:两个下标分别指向两段列表的第一个元素,两个下标所指的元素进行对比,谁小就拿出来放到一个新的列表,然后拿出的元素下标向后移动一位,继续和另一个列表的第一个元素进行对比,直到两个列表所有元素都取出来存入新列表为止;
使用归并进行归并排序
public class MergeSort {public static void main(String[] args) {int[] list = new int[]{1,2,5,7,9,3,4,6,8};int [] result = sort(list, 0, list.length-1);System.out.println(Arrays.toString(result));// merge(list, 0, (list.length-1)/2, list.length-1);}// 归并过程:将一个列表分成两段有序列表,然后合并成一个有序列表public static int[] merge(int[] list, int head, int mid, int tail) {int i = head; //第一段列表首元素int j = mid + 1; //第二段列表首元素int[] tempList = new int[tail - head + 1];int x = 0;while (i <= mid && j <= tail) { //只要两段列表都还有数,进行对比if (list[i] < list[j]) {tempList[x] = list[i];i += 1;} else {tempList[x] = list[j];j += 1;}x += 1;}// while执行完,代表有一段列表已经没有元素了while (i <= mid) { //代表第一段列表还有元素tempList[x] = list[i];i += 1;x += 1;}while (j <= tail) { //代表第二段列表还有元素tempList[x] = list[j];j += 1;x += 1;}System.out.println(Arrays.toString(tempList));return tempList;}// 归并排序public static int[] sort(int[] list, int head, int tail) {if (head < tail) { //代表还有至少两个元素,递归拆分int mid = (head + tail) / 2;sort(list, head, mid); //将左边部分排好序sort(list, mid+1, tail); //将右边部分排好序return merge(list, head, mid, tail); //将两段排好序的列表归并}return list;}
}