常见数据结构和算法实现(排序/查找/数组/链表/栈/队列/树/递归/海量数据处理/图/位图/Java版数据结构)
数据结构和算法作为程序员的基本功,一定得稳扎稳打的学习,我们常见的框架底层就是各类数据结构,例如跳表之于redis、B+树之于mysql、倒排索引之于ES,熟悉了底层数据结构,对框架有了更深层次的理解,在后续程序设计过程中就更能得心应手。掌握常见数据结构和算法的重要性显而易见,本文主要讲解了几种常见的数据结构及基础的排序和查找算法,最后对高频算法笔试面试题做了总结。本文会持续补充,希望对大家日常学习或找工作有所帮忙。本文是数据结构第一讲:排序算法
文章目录
1、什么是数据结构?(研究应用程序中数据之间逻辑关系、存储方式及其操作的学问就是数据结构)
- 程序中数据大致有四种基本逻辑结构:集合(同属一个集合)/线性关系(一对一)/树形结构(一对多)/图状结构或网状结构(多对多)
- 物理存储结构:顺序存储结构/非顺序结构(链式存储/散列结构)
- 算法的设计取决于逻辑结构;算法的实现依赖于存储结构
2、为什么学习数据结构和算法?
有3点比较重要 (王争)
- 1、直接好处是能够有写出性能更优的代码;数据结构:存储;算法:计算;
- 算法是程序的灵魂,优秀的程序可以在海量数据计算时,依然保持高速计算。
- 2、算法,是一种解决问题的思路和方法,有机会应用到生活和事业的其他方面;
- 3、长期来看,大脑思考能力是个人最重要的核心竞争力,而算法是为数不多的能够有效训练大脑思考能力的途径之一。
推荐的书籍及教程
《大话数据结构 程杰》入门
《算法图解》
《数据结构与算法分析:Java语言描述》(大学课本 伪代码)
《剑指offer》 使用的C++语言来实现的,现在我不怎么使用了
《程序员代码面试指南:IT名企算法与数据结构题目最优解》左程云,现在正在看的书
《编程珠玑》(对大数据量处理的算法)
《编程之美》(超级难)
《算法导论》(很厚很无聊)
《算法第四版》(推荐 本书没有动态规划)
《数据结构与算法 极客时间》 王争google
《算法帝国》
《数学之美》
《算法之美》(闲暇阅读) https://github.com/wangzheng0822/algo
《计算机程序设计艺术》面试必刷的宝典
《图解Java数据结构》韩顺平
《数据结构与算法之美》王争
倘若是在日常开发中,算法的基本逻辑,优缺点、适用场景是更为重要的。
如果是考察技术基础,考核的范围应该是算法的基本逻辑,优缺点、适用场景,因为这些技术点在后续具体应用中选择合适的算法来解决问题的时候很有用;如果是考察思维能力,考核的方式应该是给一个具体的算法应用题,来看看面试者的分析和思考过程,例如一道业务上曾经用到的“如何快速计算你好友的好友和你的共同好友数”。
3、有哪些常见的数据结构?
概念 | 简介 |
---|---|
数据结构 | 数组、链表(单链表/双向链表/循环链表/双向循环/静态链表)、栈(顺序栈/链式栈)、队列(双端队列/阻塞队列在线程池中大量使用/并发队列/并发阻塞队列)、散列表(散列函数/冲突解决(链表法/开放寻址)/二分快速定位元素/动态扩容/位图)、二叉树(平衡二叉树/二叉查找树/mysql底层)、树(b树/B+树/2-3树/2-3-4树)、堆(大顶堆/小顶堆/优先级队列/大数据量求topK)、图(图的存储(邻接矩阵/邻接表)/拓扑排序/最短路径/最小生成树/二分图)、跳表(链表可以快速二分查找元素)、Trie树(用于字符串补全/ES底层搜索的字符串匹配) |
算法 | 递归、排序(O(n2)冒泡/选择/插入/希尔 O(lgn)归并/快排/堆排 O(n)计数/基数/桶)、二分查找(线性表/树结构/散列表)、搜索(深度优先/广度优先/A启发式)、哈希算法、字符串匹配算法(朴素/KMP/Robin-Karp/Boyer-Moore/AC自动机/Trie树/后缀数组)、 复杂度分析(空间复杂度/时间复杂度(最好/最差/平均/均摊))、基本算法思想(贪心算法、分治算法、回溯算法、动态规划) 、其他(数论/计算几何/概率分布/并查集/拓扑网络/矩阵计算/线性规划) |
面试题 | 链表:单链表反转(把指针转向),链表中环的检测(遍历+数组保存遍历过的元素/双指针,前指针走两步,后指针走一步),两个有序的链表合并(双重遍历),删除链表倒数第n个结点(双指针,前指针比后指针先走n步),求链表的中间结点(双指针,前指针走两步,后指针走一步)等、栈:在函数调用中的应用,在表达式求值中的应用,在括号匹配中的应用(网页爬虫中< html>< script>的排除)、排序:如何在O(n)的时间复杂度内查找一个无序数组中的第 K大元素(基数排序) |
由于日常开发使用java居多,因此使用JDK提供的Java版各类数据结构更加符合实际需求。
概念 | Java版接口 | Java版抽象类 | Java版实现类 |
---|---|---|---|
数组 | Iterable | AbstractList | AbstractSequentialList , ArrayList , Vector,CopyOnWriteArrayList ,LinkedList,RoleList,RoleUnresolvedList |
队列 | Iterable | AbstractQueue | ConcurrentLinkedDeque , ConcurrentLinkedQueue ,DelayQueue,LinkedBlockingDeque,LinkedBlockingQueue,LinkedTransferQueue,PriorityBlockingQueue,PriorityQueue,SynchronousQueue |
集合 | Iterable | ConcurrentSkipListSet ,CopyOnWriteArraySet,EnumSet,HashSet,LinkedHashSet,TreeSet | |
栈 | AbstractCollection | stack |
4、说一下几种常见的排序算法和分别的复杂度,java提供的默认排序算法(数组排序)
4.1、排序算法
排序算法指标
排序方法 | 时间复杂度(表示的是一个算法执行效率与数据规模增长的变化趋势) | 最好最差情况 | 稳定性 | 最小辅助空间(表示算法的存储空间与数据规模之间的增长关系) |
---|---|---|---|---|
选择排序 | n^2 | - | 不稳定 | 空间O(1) |
4.1.1、选择排序
- 原理:将待排序的元素分为已排序(初始为空)和未排序两组,依次将未排序的元素中值最小的元素放入已排序的组中)
- 选择排序图解如下
public static void selectSort(int[] a) {
int temp,flag = 0;
int n = a.length;
for (int i = 0; i < n; i++) {
temp = a[i]; //第一个数据给temp a[i]为已排序区间的末尾
flag = i;
for (int j = i + 1; j < n; j++) {
if (a[j] < temp) {
temp = a[j]; // 值
flag = j; // 位置
}
}
if (flag != i) {
// 最小数据与第一个数据进行交换
a[flag] = a[i];
a[i] = temp;
}
}
}
4.1.2、插入排序
- 时间复杂度 n^2 空间复杂度O(1) 稳定(每次将一个待排序的元素,按其关键字的大小插入到前面已经排好序的子文件的适当位置) 经常使用
- 插入排序图解如下
public static void insertSort(int[] a) {
if (a != null) {
for (int i = 1; i < a.length; i++) {
// 寻找插入的位置
int temp = a[i], j = i;
if (a[j - 1] > temp) {
while (j >= 1 && a[j - 1] > temp) {
a[j] = a[j - 1];//依次后移
j--;
}
}
a[j] = temp;//插入合适的位置
}
}
}
4.1.3、冒泡
- n^2 稳定(相邻两元素进行比较,如有需要则进行交换)(两个for循环 一轮比较9次,二轮比较8次)
- 冒泡排序图解如下
public class 冒泡排序 {
// 冒泡排序,a表示数组,n表示数组大小
public void bubbleSort(int[] a, int n) {
if (n <= 1)
return;
for (int i = 0; i < n; ++i) {
boolean flag = false;// 提前退出冒泡循环的标志位
for (int j = 0; j < n - i - 1; ++j) {
if (a[j] > a[j + 1]) { // 交换
int tmp = a[j];
a[j] = a[j + 1];
a[j + 1] = tmp;
flag = true; // 表示有数据交换
}
}
if (!flag)
break; // 没有数据交换,提前退出
}
}
}
4.1.4、希尔排序
- 时间复杂度:nlgn~n^2 (将整个待排元素序列分割成若干个子序列,分别进行直接插入排序,待整个序列的元素基本有序,在对全体元素进行一次直接插入排序)
- 希尔排序图解如下
- 代码实现
//Java 代码实现
public class ShellSort implements IArraySort {
@Override
public int[] sort(int[] sourceArray) throws Exception {
// 对 arr 进行拷贝,不改变参数内容
int[] arr = Arrays.copyOf(sourceArray, sourceArray.length);
int gap = 1;
while (gap < arr.length) {
gap = gap * 3 + 1;
}
while (gap > 0) {
for (int i = gap; i < arr.length; i++) {
int tmp = arr[i];
int j = i - gap;
while (j >= 0 && arr[j] > tmp) {
arr[j + gap] = arr[j];
j -= gap;
}
arr[j + gap] = tmp;
}
gap = (int) Math.floor(gap / 3);
}
return arr;
}
}
4.1.5、快排
-
时间复杂度 nlgn 空间复杂度O(lgn) 不稳定 基于分割交换排序的原则,这种类型的算法占用空间较小,他将待排序列表分成三个主要部分:小于基准的元素,基准元素,大于基准的元素
- (思想:通过一次划分:将待排元素分为左右两个子序列,左侧均小于基准元素排序码,右侧均大于等于基准元素排序码,反复递归,直至每一个序列只有一个元素为止)
- 快排的优化方法,在选择基准元素时,可以(1、三数取中法(首/尾/中间各取一个数据作为分区点,取中间数作为分区点) 2、随机法)
-
快排图解如下
public static void sort(int array[], int low, int high) {
int index;
if (low >= high) {
return;
}
int i = low;
int j = high;
//基准点
index = array[i];
while (i < j) {
//由小到大排列 好吧,通过代码知道了扫描的顺序,从右开始向左扫描,若是交换了元素,从左往右扫描,然后依次进行
while (i < j && array[j] >= index) {
j--; //从右向左扫描
}
if (i < j) {//说明上述array[j]<index,while循环跳出,该值放置在基准左侧
array[i++] = array[j];
}
while (i < j && array[i] < index) {
i++; //从左向右扫描
}
if (i < j) {//说明上述array[i]>index,while循环跳出,该值放置在基准右侧
array[j--] = array[i];
}
}
//最后把基准元素放上去
array[i] = index;
sort(array, low, i - 1);
sort(array, i + 1, high);
}
编程题:用快排思想在O(n)内查找第K大元素?比如,4,2,5,12,3 这样一组数据,第3大元素就是4。
思路:选择数组区间A[0…n-1]的最后一个元素A[n-1]作为pivot,对数组A[0…n-1]原地分区,这样数组就分成了三部分,A[0…p-1]、A[p]、A[p+1…n-1],如果p+1=K,那A[p]就是要求解的元素;如果K > p+1, 说明第K大元素出现在A[p+1…n-1]区间,我们再按照上面的思路递归地在A[p+1…n-1]这个区间内查找
public class 查找无序数组的第K大的数 {
public static int kthSmallest(int[] arr, int k){
// 前置校验
if (arr == null || arr.length < k) {
return -1;
}
// 对数组 A[0…n-1] 进行分区
int partition = partition(arr, 0, arr.length - 1);
//经过一轮分区
while(partition + 1 != k){
if(partition + 1 < k){//说明第K大元素出现在A[p+1…n-1]区间
partition = partition(arr, partition + 1, arr.length - 1);
}else{//说明第K大元素出现在A[1…p-1]区间
partition = partition(arr, 0, partition - 1);
}
}
return arr[partition];//一次成功
}
private static int partition(int[] arr, int p, int r){
int pivot = arr[r];
int i = p;
for(int j = p; j <= r-1; j++){
// 这里要是 <= ,不然会出现死循环,比如查找数组 [1,1,2] 的第二小的元素 这操作真的秀
if(arr[j] < pivot){//放基准元素左侧
swap(arr, i, j);
i++;
}
}
swap(arr, i, r);
return i;
}
private static void swap(int[] arr, int i, int j){
if(i == j){
return;
}
int tmp = arr[i];
arr[i] = arr[j];
arr[j] = tmp;
}
}//时间复杂度O(n)
补充:倘若是现在开发“查找第K大元素” 这个需求,我会将这批数据放进List集合里面,然后使用Collections.sort()方法按大小排序好,然后get第K个元素。
为什么这个算法的时间复杂度为O(n)?
第一次分区查找,我们需要对大小为n的数组执行分区操作,需要遍历n个元素。第二次分区查找,我们只需要对大小为n/2的数组执行分区操作,需要遍历n/2个元素。
依次类推,分区遍历元素的个数分别为、n/2、n/4、n/8、n/16.……直到区间缩小为1。如果把每次分区遍历的元素个数加起来,就是:n+n/2+n/4+n/8+…+1。这是一个等比数列求和,最后的和等于2n-1。所以,上述解决思路的时间复杂度就为O(n)。
4.1.6、堆排
- nlgn 不稳定
- 可以看做是选择排序的改进,基于比较的排序算法,他将其输入划分为未排序和排序的区域,通过不断消除最小元素并将其移动到排序区域来收缩未排序区域。
- 归并 nlgn 稳定 jdK1.7之前集合工具包默认使用的排序算法 1.7使用的是TimSort排序方法,还没有研究过 (可分为二路归并/多路归并)
-
使用分治思想,将复杂问题分解为较小的子问题,直到分解的足够小,可以轻松解决问题为止。(将两个有序表合并成一个有序表) 由大到小排列
-
堆排图解如下
-
代码如下所示
//使用分治的思想 public static void MergeSort(int array[], int p, int r) { if (p < r) { int q = (p + r)/2; MergeSort(array, p, q); MergeSort(array, q + 1, r); Merge(array, p, q, r); } } //Merge的作用:将已经有序的A[p…q]和A[q+1…r]合并成一个有序的数组,并且放入A[p…r]。 public static void Merge(int array[], int p, int q, int r) { int i, j, k, n1, n2; n1 = q - p + 1; n2 = r - q; int[] L = new int[n1]; int[] R = new int[n2]; for(i = 0, k = p; i < n1; i++, k++){ L[i] = array[k]; } for(i = 0, k = q + 1; i < n2; i++, k++){ R[i] = array[k]; } //相当于合并两条有序的链表 由大到小排列 for(k = p, i = 0, j = 0; i < n1 && j < n2; k++){ if (L[i] > R[j]) { array[k] = L[i]; i++; } else { array[k] = R[j]; j++; } } if(i < n1){ for (j = i; j < n1; j++, k++){ array[k] = L[j]; } } if(j < n2){ for (i = j; i < n2; i++, k++){ array[k] = R[i]; } } }
4.1.7、基数排序
-
O(n) 空间复杂度O(rd) 稳定(基数排序必须依赖于另外的排序方法 实质是多关键字排序)
是通过比较数字将其分配到不同的“桶里”来排序元素的。他是线性排序算法之一。 -
解决方案:1、最高位优先法MSD 2、最低位优先法LSD
-
算法步骤
- 将所有待比较数值(正整数)统一为同样的数位长度,数位较短的数前面补零
- 从最低位开始,依次进行一次排序
- 从最低位排序一直到最高位排序完成以后, 数列就变成一个有序序列
-
基数排序图解如下
参考代码
public class RadixSort implements IArraySort {
@Override
public int[] sort(int[] sourceArray) throws Exception {
// 对 arr 进行拷贝,不改变参数内容
int[] arr = Arrays.copyOf(sourceArray, sourceArray.length);
int maxDigit = getMaxDigit(arr);
return radixSort(arr, maxDigit);
}
/**
* 获取最高位数
*/
private int getMaxDigit(int[] arr) {
int maxValue = getMaxValue(arr);
return getNumLenght(maxValue);
}
private int getMaxValue(int[] arr) {
int maxValue = arr[0];
for (int value : arr) {
if (maxValue < value) {
maxValue = value;
}
}
return maxValue;
}
protected int getNumLenght(long num) {
if (num == 0) {
return 1;
}
int lenght = 0;
for (long temp = num; temp != 0; temp /= 10) {
lenght++;
}
return lenght;
}
private int[] radixSort(int[] arr, int maxDigit) {
int mod = 10;
int dev = 1;
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;
}
}
}
return arr;
}
private int[] arrayAppend(int[] arr, int value) {
arr = Arrays.copyOf(arr, arr.length + 1);
arr[arr.length - 1] = value;
return arr;
}
}
4.1.8、桶排序
-
O(n) 将要排序的数据分到几个有序的桶里,每个桶里的数据再单独进行排序
-
适用场景:外部排序中(磁盘中,内存有限,无法将数据全部加载到内存中)
-
算法步骤
- 设置固定数量的空桶。
- 把数据放到对应的桶中。
- 对每个不为空的桶中数据进行排序。
- 拼接不为空的桶中数据,得到结果
-
动画演示
4.1.9、计数排序
-
桶排序的一种特殊形式:每个桶中的数据相同
-
算法步骤
- 设置固定数量的空桶。
- 把数据放到对应的桶中。
- 对每个不为空的桶中数据进行排序。
- 拼接不为空的桶中数据,得到结果
-
基数排序图解如下
4.1.10、排序方法的选择?
- n代表数据量
- 1、n较小,可以采用直接插入或直接选择排序
- 2、若文件初始状态基本有序,应选用直接插入、冒泡或随机的快速排序
- 3、n较大,采用复杂度为O(nlgn)的方法:快排/堆排/归并
- 4、在实际的软件开发中,为什么我们更倾向于使用插入排序而不是冒泡排序算法?
- 从代码实现上来看,冒泡排序的数据交换要比插入排序的数据移动要复杂,冒泡排序需要3个赋值操作,而插入排序只需要1个,所以在对相同数组进行排序时,冒泡排序的运行时间理论上要长于插入排序。
如果数据存储在链表中,这三种排序算法还能工作吗?
一般而言,考虑只能改变节点位置,冒泡排序相比于数组实现,比较次数一致,但交换时操作更复杂;
插入排序,比较次数一致,不需要再有后移操作,找到位置后可以直接插入,但排序完毕后可能需要倒置链表;
选择排序比较次数一致,交换操作同样比较麻烦。综上,时间复杂度和空间复杂度并无明显变化,若追求极致性能,冒泡排序的时间复杂度系数会变大,插入排序系数会减小,选择排序无明显变化。
4.2、排序工具类Arrays?如何实现一个通用的、高性能的排序函数?(Java语言采用堆排序实现排序函数,C语言使用快速排序实现排序函数)
-
Arrays拥有一组static方法(equals():比较两个array是否相等/fill():将值填入array中/sort():用来对array进行排序/binarySearch():在排好序的array中寻找元素/system.arraycopy():array的复制)
-
Jdk7中Arrays.sort()和Collections.sort()排序方法使用注意: jdk1.6中的arrays.sort()和 collections.sort()使用的是MergeSort; jdk1.7中内部实现转换成了TimSort方法,
-
对对象间比较的实现
1、有两个参数,第一个是比较的数据,第二个是比较的规则,如果comparator为空,则使用comparableTimSort的sort实现
2、传入的待排序数组若小于MIN_MERGE(Java实现中为32)则从数组开始处找到一组连接升序或严格降序(找到后翻转)的数
BinarySort:使用二分查找的方法将后续的数插入之前的已排序数组
3、开始真正的TimSort过程(选取minRun大小,之后待排序数组将被分成以minRun大小为区块的一块块子数组)
Timsort的思想:找到已经排好序的数据子序列,然后对剩余部分排序,最后合并起来 -
java提供的默认排序算法
1、对于基础数据类型,目前使用的是所谓双轴快速排序(Dual-Pivot QuickSort),是一种改进的快速排序算法,早期版本是相对传统的快速排序;
2、而对于对象数据类型,目前则是使用TimSort,思想上也是一种归并(Merge)和二分插入排序(binary Sort)结合的优化排序算法。
思路是查找数据集中已经排好序的分区(这里叫run 连续升或降的序列),然后合并这些分区来达到排序的目的。
- Java8引入了并行排序算法(直接使用parallelSort方法),这是为了充分利用现代多核处理器的计算能力,底层实现基于fork-join框架,当处理的数据集比较小的时候,差距不明显,甚至还表现差一点;但是,当数据集增长到数万或百万以上时,提高就非常大了,具体还是取决于处理器和系统环境.
- fork-join框架的适用场景:计算密集型,而非IO密集型,踩过坑
4.3、常见的查找算法?
- 1、二分查找法(考虑好边界条件,不要被面试官抓住漏洞)(使用Arrays工具类的binarySearch方法)
思路:先确定数组的中间位置,然后将要查询的值与数组中间位置的值进行比较,若小于数组中间值,则要查找的值应位于该中间值之前,依次类推;
算法: 1、如果关键字小于中央元素,只需继续在数组的前半部分进行搜索;2、如果关键字与中央元素相等,则搜索结束,找到匹配元素;3、如果关键字大于中央元素,只需继续在数组的后半部分进行搜索
限制:用于顺序链表或排序后的链表
注意事项:1、循环退出条件low<=high;2、mid的取值(low+(high-low)>>1)因为相比除法运算来说,计算机处理位运算要快得多;3、low和high的更新low=mid+1,high=mid-1
时间复杂度:O(lgn)
public int bsearch(int[] a, int n, int value) {
int low = 0;
int high = n - 1;
while (low <= high) {
int mid = (low + high) / 2;//或者int mid = low+((high-low)>>1);
if (a[mid] == value) {
return mid;
} else if (a[mid] < value) {
low = mid + 1;
} else {
high = mid - 1;
}
}
return -1;
}
- 2、4种常见的二分查找变形问题
第一种:查找第一个值等于给定值的元素
public int bsearch(int[] a, int n, int value) {
int low = 0;
int high = n - 1;
while (low <= high) {
int mid = low + ((high - low) >> 1);
if (a[mid] > value) {
high = mid - 1;
} else if (a[mid] < value) {
low = mid + 1;
} else {
if ((mid == 0) || (a[mid - 1] != value))
return mid; //mid不是第一个数或mid左边的数不是
else high = mid - 1;
}
}
return -1;
}
第二种:查找最后一个值等于给定值的元素
public int bsearch(int[] a, int n, int value) {
int low = 0;
int high = n - 1;
while (low <= high) {
int mid = low + ((high - low) >> 1);
if (a[mid] > value) {
high = mid - 1;
} else if (a[mid] < value) {
low = mid + 1;
} else {
if ((mid == n - 1) || (a[mid + 1] != value))
return mid;
else
low = mid + 1;
}
}
return -1;
}
第三种:查找第一个大于等于给定值的元素
public int bsearch(int[] a, int n, int value) {
int low = 0;
int high = n - 1;
while (low <= high) {
int mid = low + ((high - low) >> 1);
if (a[mid] >= value) {
if ((mid == 0) || (a[mid - 1] < value))
return mid;
else
high = mid - 1;
} else {
low = mid + 1;
}
}
return -1;
}
第四种:查找最后一个小于等于给定值的元素
public int bsearch7(int[] a, int n, int value) {
int low = 0;
int high = n - 1;
while (low <= high) {
int mid = low + ((high - low) >> 1);
if (a[mid] > value) {
high = mid - 1;
} else {
if ((mid == n - 1) || (a[mid + 1] > value))
return mid;
else
low = mid + 1;
}
}
return -1;
}
- 3、如果有序数组是一个循环有序数组,比如4,5,6,1,2,3。针对这种情况,如何实现一个求“值等于给定值”的二分查找算法呢?
public int search(int[] nums, int target) {
if (nums.length == 1 && nums[0] == target)
return 0;
int left = 0;
int right = nums.length - 1;
int mid = 0;
while (left < right) {
mid = (left + right) >> 1;
if (nums[left] == target)
return left;
if (nums[right] == target)
return right;
if (nums[mid] == target)
return mid;
if (nums[mid] > nums[left]) { // 第一种情况
if (target > nums[mid]) {
left = mid + 1; //在mid到左侧最大值区间
} else {//target小于中间值
if (target > nums[left]) {
right = mid - 1;
} else {
left = mid + 1; //在右侧区间
}
}
} else { // 第二种情况 mid小于最左值
if (target > nums[mid]) {//两种情况:1、在mid右侧 2、在左侧
if (target < nums[right]) {
left = mid + 1; //1、在mid右侧
} else {
right = mid - 1; //2、在左侧
}
} else { //在右侧的左边区域
right = mid - 1;
}
}
}
return -1;
}
- 4、x的平方根 LeetCode69 实现int sqrt(int x)函数。计算并返回x的平方根,其中x是非负整数,由于返回类型是整数,结果只保留整数的部分,小数部分将被舍去
- 方法1:java自带API
public int mySqrt(int x) {
return (int)Math.sqrt(x);
}
- 方法2:二分搜索
int mySqrt(int x) {
//注:在中间过程计算平方的时候可能出现溢出,所以用long long。
long i=0;
long j=x/2+1;//对于一个非负数n,它的平方根不会大于(n/2+1)
while(i<=j)
{
long mid=(i+j)/2;
long res=mid*mid;
if(res==x) return mid;
else if(res<x) i=mid+1;
else j=mid-1;
}
return j;
}
方法3:牛顿迭代法 求c的算术平方根就是求f(x)=x^2-c的正根 迭代公式:xn+1=1/2(xn+c/xn)
int mySqrt(int x) {
if (x == 0) return 0;
double last=0;
double res=1;
while(res!=last)
{
last=res;
res=(res+x/res)/2;
}
return int(res);
}
4.4、复杂度分析
常见的时间复杂度?(表示的是一个算法执行效率与数据规模增长的变化趋势)
时间复杂度 | 概念 |
---|
- O(1) 常数阶| 常量级别的时间复杂度:只要代码的执行时间不随n的增大而增长,这样代码的时间复杂度我们都记作O(1)。
2、O(logn)对数阶、O(nlogn)线性对数阶| 代码循环执行的次数呈现对数关系
3、O(m+n)、O(m*n) | 代码的复杂度由两个数据的规模来决定
空间复杂度:(表示算法的存储空间与数据规模之间的增长关系)
常见的空间复杂度就是O(1)、O(n)、O(n2)
- 平均时间复杂度(加权平均时间复杂度):加了概率
- 均摊时间复杂度:对一个数据结构进行一组连续操作中,大部分情况下时间复杂度都很低,只有个别情况下时间复杂度比较高,而且这些操作之间存在前后连贯的时序关系,这个时候,我们就可以将这一组操作放在一块儿分析,看是否能将较高时间复杂度那次操作的耗时,平摊到其他那些时间复杂度比较低的操作上。
- 算法的最好情况和最坏情况?
最好情况:算法执行最佳的数据排列。如:二分搜索时,目标值正好位于搜索的数据中心,时间复杂度为0;
最差情况:给定算法的最差输入。如:快速排序中,如果选择的关键值是列表中最大或最小值,最差情况就会发生,时间复杂度会变成O(n^2)
4.5、如何高效地判断无序数组中是否包含某特定值?
// 方法1:使用list (**最常使用**)
public static boolean useList(String[] arr, String targetValue) {
return Arrays.asList(arr).contains(targetValue);
}
// 方法2:使用Set 低效
>public static boolean useSet(String[] arr, String targetValue) {
Set<String> set = new HashSet<String>(Arrays.asList(arr));
return set.contains(targetValue);
}
// 方法3:使用一个简单循环 最高效
>public static boolean useLoop(String[] arr, String targetValue) {
for(String s: arr){
if(s.equals(targetValue))
return true;
}
return false;
}
- 方法4:Arrays.binarySearch()方法:数组必须是有序的(有序数组时,使用列表或树可达到O(lgn),使用hashset可达到O(1))
4.6、查找算法实战?
-
1、我们要给电商交易系统中的“订单”排序。订单有两个属性(下单时间,订单金额) 需求是按金额从小到大对订单数据排序。对金额相等的订单,按下单时间从早到晚排序
稳定性概念:如果待排序的序列中存在值相等的元素,经过排序之后,相等元素之间原有的先后顺序不变
思路:先按下单时间给订单排序,排完序之后,使用稳定排序算法,按订单金额重新排序(稳定排序算法可以保持金额相同的两个对象,在排序之后的前后顺序不变) -
2、O(n)时间复杂度内求无序数组中的第K大元素?(利用分区的思想) 代码放在eclipse中
我们选择数组区间A[0…n-1]的最后一个元素A[n-1]作为pivot,对数组A[0…n-1]原地分区,这样数组就分成了三部分,A[0…p-1]、A[p]、A[p+1…n-1]。
如果p+1=K,那A[p]就是要求解的元素;如果K>p+1, 说明第K大元素出现在A[p+1…n-1]区间,我们再按照上面的思路递归地在A[p+1…n-1]这个区间内查找。同理,如果K<p+1,那我们就在A[0…p-1]区间查找。 -
3、现在你有10个接口访问日志文件,每个日志文件大小约300MB,每个文件里的日志都是按照时间戳从小到大排序的。你希望将这10个较小的日志文件,合并为1个日志文件,合并之后的日志仍然按照时间戳从小到大排列。如果处理上述排序任务的机器内存只有1GB,你有什么好的解决思路
answer:先构建十条io流,分别指向十个文件,每条io流读取对应文件的第一条数据,然后比较时间戳,选择出时间戳最小的那条数据,将其写入一个新的文件,然后指向该时间戳的io流读取下一行数据,然后继续刚才的操作,比较选出最小的时间戳数据,写入新文件,io流读取下一行数据,以此类推,完成文件的合并, 这种处理方式,日志文件有n个数据就要比较n次,每次比较选出一条数据来写入,时间复杂度是O(n),空间复杂度是O(1),几乎不占用内存。