# 课程目标 # ## 目的 ## * 找工作,面试中经常会出现数据结构和算法的问题,尤其是笔试中经常出现。 * 数据结构与算法这门课程是初级程序员向中高级程序员迈进的必经之路。 * 要进大公司,数据结构与算法是不可或缺的。 * 有时候看别人的源码的时候,经常会涉及到。 ## 基本算法 ## * 冒泡排序 * 选择排序 * 插入排序 ## 递归算法 ## * 自然数的累加问题(1+....+99+100) * 兔子繁衍问题:已知一对兔子每一个月可以生一对小兔子,而一对兔子出生后.第三个月开始生小兔子假如一年内没有发生死亡,则一对兔子一年内能繁殖成多少对? * 汉诺塔 * 快速排序算法 ## 数据结构与算法的关系 ## * 循环数数,n个人围成一圈报数,从1开始,凡报到3的退出,问留到最后的是几号? * 括号算法(简单的编译器),一行字符串有小括号、中括号、大括号,计算括号匹配与否,是否错位。 * 计算器的算法。四则运算 * 双链表 * 二叉树 # 三种基本的算法 # ## 冒泡排序 ## **从第一个数据开始,两两比较,如果左边的大于右边的,那就交换两个数据的位置,依次这样往下比较。会找到一个最大值放到数据的最后,然后下一次按上述方式继续冒泡。冒泡次数为n-1。** ![bubble](bubbleSort.png) public static void bubbleSort(int[] datas){ //外层循环,控制排序次数 for (int j = datas.length - 1; j > 0 ; j--) { //内层循环,控制冒泡次数 for (int i = 0; i < j; i++) { if (datas[i] > datas[i + 1]) { //交换两个数的位置 change(datas,i, i + 1); } } } } ## 选择排序 ## **一次过程:(从小到大),从所有数据中找最小数据的下标,找到下标和第一个位置交换。** ![select](selectSort.png) /** * 选择排序 * @param datas */ public static void selectSort(int[] datas){ //外层循环,从0开始 for (int i = 0; i < datas.length - 1; i++) { int minIndex = i; for (int j = i + 1; j < datas.length; j++) { if (datas[j] < datas[minIndex]) { minIndex = j; } } change(datas, minIndex, i); } } ## 插入排序 ## **从第二个位置开始,依次向前比较所有数据,一直找到比该数据小的位置,结束过程,并把该数据插入到(比该数据小)的位置后面** /** * 插入排序 * @param datas */ public static void insertSort(int[] datas){ //外层循环控制执行次数 for (int i = 1; i < datas.length; i++) { int temp = datas[i]; int j = i - 1; for (; j >= 0; j--) { if (datas[j] > temp) { datas[j + 1] = datas[j]; }else { break; } } datas[j + 1] = temp; } } # 递归算法 # **递归效率高的原因:直接在内存中进行压栈和出栈处理** ## 一个简单的递归例子 ## /** * 求1到的和num * @param num * @return */ public static int sum(int num){ if (num < 1) { throw new RuntimeException("请按套路出牌!"); }else if (num == 1) { return 1; }else { return num + sum(num - 1); } } ## 兔子繁衍问题:已知一对兔子每一个月可以生一对小兔子,而一对兔子出生后.第三个月开始生小兔子假如一年内没有发生死亡,则一对兔子一年内能繁殖成多少对? ## /** * 求第n个月的兔子对数 * @param n * @return */ public static int tuzi(int n){ if (n == 1 || n == 2) { return 1; }else { return tuzi(n-1) + tuzi(n-2); } } ![tuzi](tuzi.png) ## 汉诺塔问题 ## /** * 汉诺塔,n个盘,从a柱借助b柱移动到c柱 * @param n * @param a * @param b * @param c */ public static void TowerOfHanoi(int n,char a,char b, char c){ if (n == 1) { System.out.println("盘" + n + "从" + a + "柱移动到" + c + "柱"); }else { TowerOfHanoi(n - 1, a, c, b); System.out.println("盘" + n + "从" + a + "柱移动到" + c + "柱"); TowerOfHanoi(n-1, b, a, c); } } ## 快速排序 ## **以最后一个数据做参照物,定义左右指针(可以理解成下标),左指针从左往右依次找数据,找比参照物大的数据,右指针从右往左找比参照物小的数据,找到就停止,如果左右指针没有交叉,就交换左右指针的位置上的值,继续找数据,如果交叉,终止,交换左指针和参照物的值。由左指针把数据分成两半,每一半按同样的方式去找。** /** * @param datas * @param left * 数据的起始位置 * @param right * 数据的结束位置 */ public static void quickSort(int[] datas,int left,int right){ //跳出递归的条件,如果交叉 if (left > right) { return; } //否则,及进行快速排序 int middle = findMiddle(datas, left, right); //每一半按照同样的方式排序 //左一半的数据 quickSort(datas, left, middle - 1); //右一半 quickSort(datas, middle + 1, right); } /** * 找中间位置 * @param datas * @param left * @param right * @return */ public static int findMiddle(int[] datas,int left,int right){ int middle = -1; //以最后一个数据作为参照物 int temp = datas[right]; int leftIndex = left; int rightIndex = right - 1; while (true) { //1.左指针从左往右找比temp大的 while (leftIndex < right && datas[leftIndex] <= temp) { //跳出循环的条件有两个,第一个是leftIndex == right,第二个datas[leftIndex] >temp //如果通过 leftIndex == right跳出循环,说明参照物是最大值,那么middle就是right。 if (leftIndex == right) { middle = leftIndex; break; } leftIndex ++; } while(rightIndex >= left && datas[rightIndex] >= temp){ //如果通过rightindex >= left跳出循环,说明参照物是最小的 //这个条件也可以按交叉处理,按交叉的处理 rightIndex --; } if (leftIndex < rightIndex) { //没有交叉 //交换左右指针上的数据 //继续循环 change(datas, leftIndex, rightIndex); continue; }else { //交叉 //交换左指针与参照物的值 change(datas, leftIndex, right); //找到中间点了(leftIndex),终止整个循环 middle = leftIndex; break; } } return middle; } **四种排序的速度测试** # 数据结构和算法的关系 # ## 连续数数的案例(队列) ## ### 使用数组实现 ### ![arrayCount](suzuCount.png) /** * 用数组解决数数的问题 * * @param personNum * 人数 * @param num * 数到num退出 */ public static void count(int personNum, int num) { // 将人放到数组中 int[] persons = new int[personNum]; for (int i = 0; i < persons.length; i++) { persons[i] = i + 1; } // 定义一些变量 int duns = 0;//蹲下的人数 int dunCounts = 0;//蹲下了,但是还数数了的次数,就是多余的数数次数 int index = 0;//数到了几 while(duns != personNum){ //如果不是全部人都蹲下了,那就继续数数 //判断当前数数是否是蹲下人数数 if (persons[index % personNum] == 0) { //如果是,则 dunCounts ++; //继续数数 index ++; }else { ///判断当前数数位置是否需要蹲下 if ((index + 1 - dunCounts) % num == 0) { //如果需要 System.out.println(persons[index % personNum] + "滚出去!!!"); persons[index % personNum] = 0; duns ++; //继续数数 index ++; }else { //否则,继续数数 index ++; } } } } ### 队列数数 ### /** * 用队列的方式来数数 * @param personNum * @param num+ */ public static void count2(int personNum,int num){ //将人放入队列中 Queue queue = new LinkedList(); for(int i=0;i getNums(String exp) { List nums = new ArrayList(); StringTokenizer sTokenizer = new StringTokenizer(exp, "+-*/"); while (sTokenizer.hasMoreElements()) { String str = sTokenizer.nextElement().toString().trim(); //如果str的第一个字符是@,那么转成负号 if (str.charAt(0) == '@') { str = "-" + str.substring(1); } double d = Double.parseDouble(str); nums.add(d); } return nums; } ### 提取运算符集合 ### /** * 提取运算符 * @param exp * @return */ private static List getOpts(String exp) { List opts = new ArrayList(); StringTokenizer sTokenizer = new StringTokenizer(exp, "0123456789.@"); while (sTokenizer.hasMoreElements()) { String str = sTokenizer.nextElement().toString().trim(); opts.add(str.charAt(0)); } return opts; } ### 不带括号的四则运算 ### * 1.0先将“-”转换成“@”。 * 2.0提取数字集合。 * 3.0提取字符集合。 * 4.0先乘除。 * 5.0后加减。 /** * 不带括号的四则运算 * @param exp * @return */ public static double calc(String exp){ //负号转成@ exp = fu2At(exp); //提取数字集合 List nums = getNums(exp); //提取运算符集合 List opts = getOpts(exp); //先乘除 //遍历出所有的*/号 for(int i=0;i"-3+(3*-6)-8/(-4-3)" exp = exp.substring(0,leftIndex) + d + exp.substring(rightIndex + 1); System.out.println(exp); //对exp做递归运算 return calcKuohao(exp); }else { //说明没有括号 return calc(exp); } } # 自定义数据结构 # ## 单链表 ## **链表添加数据的步骤** * 新建节点。 * 把数据放到节点中。 * 把节点放入链表中。 **应用场景** * 删除时,只适合删除第一个元素; * 添加时,只直接添加到最后一个元素的后面或者添加到第一个元素的前面; * 属于单向迭代器,只能从一个方向走到头(只支持前进或后退,取决于实现),查找效率极差。不适合大量查询的场合。 ***这种典型的应用场合是各类缓冲池和栈的实现。*** ## 双链表 ## ### 双链表和单链表的区别和应用场景 ### ***单链表只能查找下一个数据,双链表可以查找上一个和下一个节点的数据*** ***应用场景*** * 删除时,可以删除任意元素,而只需要极小的开销; * 添加时,当知道它的前一个或后一个位置的元素时,只需要极小的开销,可以从任意位置添加。 * 属于双向迭代器,可以从头走到尾或从尾走到头,但同样查找时需要遍历,效率与单向链表无改善,不适合大量查询的场合。 ***这种典型的应用场景是各种不需要排序的数据列表管理。*** ## 添加数据 ## ### 双链表从尾部添加数据 ### /** * 从尾部添加数据 * @param data */ public void addFromRear(Object data){ //将数据放入节点中 Node node = new Node(data); //将节点放入链表中 if (head == null) { //如果头节点为空,说明链表为空,那么头和尾节点都是node head = node; rear = node; }else { //若有头节点 //尾节点的下一个节点是node rear.next = node; //node的上一个节点是rear node.prev = rear; //将node设为尾节点 rear = node; } } ### 打印双链表 ### @Override public String toString() { StringBuilder sBuilder = new StringBuilder("["); //拼接数据 //遍历链表 Node node = head; //如果头节点不为空则,一直遍历 while(node != null){ if (node != rear) { sBuilder.append(node.data + ","); }else{ sBuilder.append(node.data + "]"); } //变更node,要不就死循环了 node = node.next; } return sBuilder.toString(); } ### 从头部添加数据 ### /** * 从头部添加数据 * @param data */ public void addFromHead(Object data){ //创建新节点,把数据放入节点中 Node node = new Node(data); //将节点放入链表中 if (head == null) { //如果头节点为空,说明链表没数据 head = node; rear = node; }else { //如果链表有数据 //从头部添加数据,头节点的前一个节点就是node head.prev = node; //node的下一个节点是head node.next = head; //将node变成head head = node; } } ### 排序添加数据(添加进来后自动排序) ### * 1.object比较数据大小 * 2.找位置,(从小到大),找比它大的第一个节点。 * 3.如果没找到比它大的第一个节点,那就说明data最大,则放到链表尾部。 * 4.如果找到了 /** * 添加数据并排序 * @param data */ public void addSort(Object data){ //创建节点,将数据放入节点中 Node node = new Node(data); //找位置 Node next = findNext(data); if (next == null) { //如果找不到比它大的,说明他是最大的\ //那么添加到尾部 addFromRear(data); }else { if (next == head) { addFromHead(data); }else { //否则,将node插入中间,指定node的prev和next,并有一个节点的prev和一个节点的next指向node //next的上一个节点的下一个节点是node next.prev.next = node; //node的下一个节点是next node.next = next; //node的上一个节点是next的上一个节点 node.prev = next.prev; //将它添加到next的前面 next.prev = node; } } } ### 比较两个Object的大小 ### /** * 比较两个数据的大小 * @param d1 * @param d2 * @return */ public int compare(Object d1,Object d2){ Comparable c1 = null; Comparable c2 = null; //如果Object类型实现了比较器 if (d1 instanceof Comparable && d2 instanceof Comparable) { c1 = (Comparable) d1; c2 = (Comparable) d2; }else { c1 = d1.toString(); c2 = d2.toString(); } return c1.compareTo(c2); } ### 查找比data大的第一个节点 ### /** * 查找比data大的第一个节点 * @param data * @return */ public Node findNext(Object data){ //从头节点开始遍历 Node node = head; while(node != null){ if (compare(node.data, data) > 0) { //如果next的数据比data大 break; }else { //否则,继续找 node = node.next; } } return node; } ## 删除数据 ## ### 1.查找数据所在的节点 ### /** * 查找data所在的节点 * @param data * @return */ private Node findNode(Object data) { //从头节点开始遍历 Node node = head; while(node != null){ if (node.data.equals(data) && node.data.hashCode() == data.hashCode()) { //找到了就跳出循环 break; }else { //否则,继续找 node = node.next; } } //返回node,如果找到了,break跳出那就返回找到的node,如果找不到node == null跳出,那就返回null。 return node; } ### 2.删除该数据所在的节点 ### * 只有一个节点 * 删除头节点 * 删除尾节点 * 删除中间节点 /** * 删除某个节点 * @param node */ public void delete(Node node){ //1.如果只有一个节点 if (node == head && node == rear) { //要删除的话,让头节点和尾节点为空即可 head = null; rear = null; }else if (node == head) { //2.如果该节点是头节点,说明后面肯定有节点 //断开头节点对node的引用 head = head.next; //断开下一个节点的prev对node的引用 head.prev = null; }else if (node == rear) { //3.如果该节点是尾节点,说明前面肯定还有节点 //断开尾节点对node的引用 rear = rear.prev; //断开前一个节点的next对node的引用 rear.next = null; }else { //4.如果该节点是中间节点,说明两边肯定都有节点 //node的前一个节点的next指向node的下一个节点 node.prev.next = node.next; //node的后一个节点的prev指向node节点的前一个节点 node.next.prev = node.prev; } } ## 修改数据 ## ### 1.传入oldData和newData来删除数据 ### /** * 更新数据 * @param oldData * @param newData */ public void updata(Object oldData,Object newData){ //先找到oldData所在的节点 Node node = findNode(oldData); if (node != null) { node.data = newData; } } ### 2.只传入newData来删除数据 ### **应用场景:特定的一些场景,比如要修改一个人的名字,我们根据传入的人的身份证号来获取这个人的信息,然后再修改名字** public class Person { public String id; public char sex; public String name; public Person(String id,char sex,String name) { this.id = id; this.name = name; this.sex = sex; } @Override public boolean equals(Object obj) { if (obj instanceof Person) { //如果是Person类型 //比较身份证号,身份证一样才是一样的 Person p = (Person) obj; return this.id.equals(p.id); }else { //如果不是人,那直接不一样 return false; } } @Override public int hashCode() { return id.hashCode(); } @Override public String toString() { return this.name; } } ## 查找数据 ## ### 是否包含某数据:contains()方法 ### ### 迭代器 ### **实现Iterable接口** @Override public Iterator iterator() { class MyIterator implements Iterator{ //从头节点开始遍历 Node node = head; @Override public boolean hasNext() { //是否有节点 return node != null; } @Override public Object next() { //先获取node的数据 Object data = node.data; //将node设为下一个节点 node = node.next; return data; } @Override public void remove() { //调用next方法之后,node就变成了以前的node的下一个节点(此处要考虑此时的node是否为空) //那就是要删除现在的node的上一个节点 if (node != null) { delete(node.prev); }else { //如果node为空,说明要删除的节点是尾节点 delete(rear); } } } return new MyIterator(); } ## 双链表封装成泛型 ## ## 链表实现队列 ## **队列,从队列尾部添加数据,从队列头移除数据** * size()方法 @Override public int size() { return datas.count; } * isEmpty()方法 //MyQueue的isEmpty方法 @Override public boolean isEmpty() { return datas.isEmpty(); } //MyDoubleLink的isEmpty方法 public boolean isEmpty(){ return head == null; } * add()方法 @Override public boolean add(E e) { datas.addFromRear(e); return true; } * poll()方法 @Override public E poll() { //取出数据 E data = datas.deleteFromHead(); return data; } /** * 从头节点移除数据,并返回删除的数据 * @param data * @return */ public E deleteFromHead(){ E data = null; if (head != null){ data = head.data; //下一个数据成为头节点 head = head.next; //判断后面没有 if (head != null) head.prev = null; else { //没有数据 rear = null; } } return data; } ## 链表实现堆栈 ## **栈,从尾部添加数据,从尾部移除数据** public class MyStack { private MyDoubleLink2 datas = new MyDoubleLink2(); /** * 压栈 * @param data */ public void push(E data){ //从尾部添加数据 datas.addFromRear(data); } /** * 出栈 * @return */ public E pop(){ //从尾部取数据 E data = datas.deleteFromRear(); return data; } public boolean isEmpty(){ return datas.isEmpty(); } } ## 二叉树 ## ### 基本特点 ### 每个节点最多有两个子节点,除了根节点之外,每个节点都有一个唯一的父节点。 ### 有序二叉树 ### 查找快,删除数据也快。 **数据库中的索引,例如b-树索引** **二叉树遍历** * 前序: 根节点在前 * 中序: 根节点在中 * 后序: 根节点在后 **二叉树有序、不可重复的添加节点** * 判断data是否存在(查找data所在的节点),如果存在,则return * 否则,创建节点。 * 将数据放入节点中。 * 找父节点。:判断如果root == null,那么添加节点为root。 * 否则,就找父节点。 * 将找到的父节点设为新节点的父节点。 * 比较新增数据和父节点数据的大小,如果新增数据大,那么它为父节点的右子节点,否则为左子节点。 /** * 添加数据 * @param data */ public void add(Object data){ //判断是否有节点包含data数据 if (contains(data)) { //直接返回 return; }else { //否则创建节点,将数据放入节点中 Node node = new Node(data); //找父节点 if (root == null) { //说明二叉树为空 root = node; }else { //二叉树不为空 Node parent = findParent(data); //设置为新节点的父节点 node.parent = parent; //比较数据data和parent的数据大小 if (compare(data, parent.data) > 0) { //如果比parent大,则放到右边 parent.right = node; }else { parent.left = node; } } } } ***查找data所在的节点*** * 从根节点开始遍历。 * while(node != null) * 如果node.data.equals(data) &&node.data.hashCode() == data.hashCode()则找到了,break退出循环。 * 如果没找到,则比较data和node.data的大小,则从右边找,否则从左边找。 /** * 查找数据所在节点 * @param data */ private Node findNode(Object data) { //从根节点开始遍历 Node node = root; while(node != null){ //比较node.data和data if (node.data.equals(data) && node.data.hashCode() == data.hashCode()) { //找到数据了,直接break break; }else { //否则,比较两个数据的大小,看看是往左还是往右找 if (compare(data,node.data) > 0){ //说明data比节点数据大,则往右找 node = node.right; }else { node = node.left; } } } return node; } ***查找新增数据的父节点*** * 从根节点开始遍历。 * 定义一个parent节点 * while(node != null) * parent = node; * 比较当前节点数据和新增数据的大小,如果新增数据大则往右找,否则往左找。 * 如果node为空了,那么上一次记录的parent就是父节点。 /** * 找父节点 * @param data * @return */ private Node findParent(Object data) { //从根节点开始遍历 Node node = root; Node parent = null; while(node != null){ //每次记录可能的父节点 parent = node; //比较数据的大小 if (compare(data,node.data) > 0) { //如果data比节点的数据大 //往右找 node = node.right; }else { //否则往左找 node = node.left; } } return parent; } ***遍历察看所有的数据*** 递归 privete void see(Node node){ if(node != null){ see(node.left); system.out.println(node.data); see(node.right); } **删除数据** ***删除根节点*** * 没有子节点 * 只有左节点 * 只有右节点 * 两个子节点 ***非根节点*** * 没有子节点 * 只有左节点 * 只有右节点 * 两个子节点 ***删除数据的步骤:*** * 查找数据所在的节点 * 删除数据 /** * 删除数据 * ***删除根节点*** * 没有子节点 * 只有左节点 * 只有右节点 * 两个子节点 ***非根节点*** * 没有子节点 * 只有左节点 * 只有右节点 * 两个子节点 * @param data */ public void delete(Object data){ //首先查找数据所在节点 Node node = findNode(data); if (node != null) { //如果节点存在 //1.是根节点 if (node == root) { //1.1没有子节点 if (node.left == null && node.right == null) { //直接将根节点置为空即可 root = null; }else if (node.left == null) { //只有右儿子 //root指向右儿子,右儿子继位 root = root.right; //父皇驾崩,右儿子没有爹了 root.parent = null; }else if (node.right == null) { //只有左二子 //左二子继位 root = root.left; root.parent = null; }else { //有两个儿子 //规定左二子继位 //分裂两个子节点,让保留左儿子,让右儿子放到左二子的最右边 Node left = split(node); //将左二子设为根节点 root = left; root.parent = null; } }else { //如果不是根节点 if (node.left == null && node.right == null) { //如果没有子节点 //看该节点是左儿子还是右儿子 if (compare(node.data, node.parent.data) > 0) { //右儿子 node.parent.right = null; }else { //左儿子 node.parent.left = null; } }else if (node.left == null) { //如果只有右儿子 //爷爷变父亲 node.right.parent = node.parent; //比较要删除的节点与父节点的大小,确定要当做左儿子还是右儿子 if (compare(node.data, node.parent.data) > 0) { //如果比父节点大,则当成右节点 node.parent.right = node.right; }else { node.parent.left = node.left; } }else if (node.right == null) { //如果只有左儿子 //爷爷变父亲 node.left.parent = node.parent; //比较要删除的节点与父节点的大小,确定要做左儿子还是右儿子 if (compare(node.data, node.parent.data) > 0) { //如果node大,则是右儿子 node.parent.right = node.left; }else { //左儿子 node.parent.left = node.left; } }else { //如果两边都有儿子 //先分裂节点 Node left = split(node); //得到了左儿子 //左儿子继位 left.parent = node.parent; //比较node和node的父节点的 if (compare(node.data, node.parent.data) > 0){ //右儿子 node.parent.right = left; }else { //左儿子 node.parent.left = left; } } } } } ***分裂一个节点的两个儿子,保留左儿子,将右儿子放到左儿子的最右边*** /** * 分裂一个节点的两个子节点,保留左儿子,将右儿子放到左儿子的最右边 * @param node */ private Node split(Node node) { //得到左儿子 Node left = node.left; //给右儿子找父节点,从left开始找 Node parent = findParent(left, node.right.data); //将parent设为右儿子的父节点 node.right.parent = parent; //parent的右儿子是node.right parent.right = node.right; return left; }