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