赞
踩
/* 3 1 2 4 2 1 3 4 1 2 3 4,你选择随机或者第一个都行,因为由于right和left的操作,所以通常会打乱原来的顺序,因为需要得到比2小的和比2大的(之所以是通常,看后面给出的原因即可) 然后继续分开,那么就是1 2和3 4,直接判断即可 5 6 8 7 6 5 8 7 5 6 8 7,这里可以发现一个问题,我们选择了6,结果确相同,而我们选择5就是6 5 8 7(left加了1的情况),当然再次的选择6 又会变成5 6 8 7了(但再次的选择一般都会分开,只是随机可能导致不变,所以我们一般都取第一个),所以可以发现,即这个时候可以发现,随机的取或者取第一个,由于因为我们选择的地方,左右操作始终只是在第一个left,且后面的right必然到第一个left,所以基本循环了,当然,这是分治的一个隐藏的重合,但并没有什么问题,因为你可以通过判断,来使得如果第一个right直接到left,那么就直接的取基准而不用判断left了(当然,一般他后面有具体相同的操作,所以我们也不会进行判断,因为就算你判断了,不也是需要交换,那么总体还是不好,因为你的判断,其他情况都需要操作,即判断多了,因为总不能为了你一个小提升,而牺牲其他情况吧,一般来说,只有总体的提升,我们才会进行操作,比如后面的移动,就能节省两次判断,而我们只需要一个判断就可,且他是最后判断的,所以有显示的提高),当然就算重合了也没有关系,反正会操作继续分开,只是加上我们的判断,可以稍微提高那么一丢丢的性能,但不建议(上面括号里面已经说明了) 然后继续分开,因为操作一次就要分开,因为都是在第二个即5的为(取6)或者6的位置(取5)重合 所以我们分成5 6和8 7 现在,我们直接判断即可,不用取了,因为会导致交换或者不变(因为取第一个,那么直接交换,取第二个,那么操作了两次交换,一个首部交换,一个重合交换,那么不变) 那么判断之后,就是5 6和7 8了,在程序里有体现,当然,上面是因为先将left+1的,所以需要有判断(防止5 6交换),如果不操作加1,那么自然就会自动的排序好的,而不会进行与第二个交换,因为与自己交换了(即与第一个,对应与5 6中的5,所以使得自动排序,你可能不懂,看代码即可,后面会说明) 从上面看,我们可以知道,我们选择的基准,会持续的以我们选择的进行分开,所以可以发现,他的选择,好像与二叉树的节点一样,只是他没有操作自平衡,即他的这个操作我们可以大致的认为是O(logn),范围是O(n)到O(logn),即前面的[10~1],没有0.99,如果刚好都是一半,那么可以认为是O(logn),而如果取的是这样的,比如1 2 3 4 5 6 7 8,我取8,然后慢慢的减1,可以是如下: 1 2 3 4 5 6 7 8(因为我们不知道他是否排序好的) 8 2 3 4 5 6 7 1 1 2 3 4 5 6 7 8 分开变成1 2 3 4 5 6 7和8(单独的不用操作),那么以此类推 第二次:1 2 3 4 5 6 第四次:1 2 3 4 第七次:1,即第七次得到,很明显,与之前的冒泡一样,也是8-1=7次,所以这种情况我们也会认为是O(n)(n代表拆分几次,一般时间复杂度的n代表执行次数,虽然前面说过代表总数,但是是对于他们那里表示的,实际上总数也可以是总次数,比如循环的次数,所以n的意思还是没有变的,即是总数) 但总体来说,他的交换(即分开,注意是分开),是O(logn)(因为需要极端情况下,才会是O(n),类似于二叉树,其他情况下,由于省略的关系,基本都会是O(logn)),但是他也只是交换(分开)而已,他们本身需要进行对比,即right和left的判断大小移动,那么他是多少次呢,很明显,总体比较7次(因为基准占用一次,且结合分开的来说就是7次,只是只有分开的7次),那么我们可以认为他的比较是O(n)(操作了省略),虽然是n-1(这里的n代表总数值长度) 而正是因为是分开的7次,且分开可能只需要3次即可,即logn,所以总时间复杂度就是O(nlogn) 这里只是进行解释,具体还是看后面的代码来体会的 */
package com.lagou; import java.util.Arrays; /** * */ public class test4 { //由于双边循环和单边循环只是交换不同,所以可以使用他们的交换来操作 //双边循环 public static int two(int[] arr, int start, int end) { //我们得到了数值,以及他的起始操作和结尾操作的下标,我们只是给这个具体下标范围进行排序 //首先,我们定义一个对应的支点pivot int pivot; //我们将第一个下标作为基准 pivot = start; //实际上可以直接得到具体数值,来省略一些代码,你可以自己操作就行了,这里就不改变了 //定义left,这是操作了+1,即前面说的,在程序里加1即可 int left = start + 1; //定义right int right = end; while (true) { while (left < right && arr[right] > arr[pivot]) { //即,如果是相同的,也认为是小的,所以到最后,也可以操作排列,因为他对于我来说是相同,但对于其他来说可不是了 right--; } while (left < right && arr[left] < arr[pivot]) { //即,如果是相同的,也认为是大的,所以到最后,也可以操作排列,因为他对于我来说是相同,但对于其他来说可不是了 left++; } //上面都是操作while循环,所以只有对应的地方才会停止 //而操作left < right,是使得他们重合后,不进行移动,否则可能出现right已经重合了,但是left还移动了 //这种情况也容易导致越界,因为left++,在最后时刻,可能超过了数组下标,比如,8 7,那么7后面还是会移动的,而操作了left<right就能避免上面说明的情况 //如果还是left<right,即没有重合 if (left < right) { int temp = arr[left]; arr[left] = arr[right]; arr[right] = temp; //操作了交换 //交换后,我们可以发现,他实际上操作了一次必定的移动,实际上我们可以使得right和left进行移动 right--; left++; //因为是right先操作,所以我们交换right即可,即需要后面的right--;使得去操作交换,这使得原来需要两个判断,只需要一个判断了,并且这个判断是最后才操作的,所以有显示的提高 } //之所以加上大于是防止特殊情况,比如使得无限循环,因为上面基本不会操作下标了 if (left >= right) { if (arr[right] > arr[pivot]) { //防止先移动后,出现了大的情况,因为我们需要交换使得左下右大 right--; //除了防止上面的移动,也防止会与第二个进行交换(特别的是只剩下2个时,比如前面解释的5 6),因为我们手动操作int left = start + 1;,所以虽然程序里加了1,但是为了防止出现与第二个操作交换,这里也需要进行-- //你可能认为,我们不操作int left = start + 1;即可,但是确需要再次的判断,反正这里也操作了--,为什么不继续利用呢,所以要随机应变,当然,如果不操作上面的移动,那么你也可以不操作加1,即这里也可以不判断,你可以都进行删除,发现,结果也是一样的 } //到这里代表重合 //那么可以不交换吗,答:不能 //如果不交换,那么对于分开来说,由于第一个必然会比之前的左边大,那么他会一直操作到最后一个,且由于不会交换,那么就会出现无限循环 int temp = arr[right]; arr[right] = arr[pivot]; arr[pivot] = temp; //我们直接的退出 break; } } return right; //这个位置代表重合的位置 } //单边循环 public static int one(int[] arr, int start, int end) { int pivot; //我们将第一个下标作为基准 pivot = start; //定义mark,也是初始位置 int mark = start; for (int i = start + 1; i <= end; i++) { if (arr[pivot] > arr[i]) { //如果i=mark,且是小的,那么直接make++即可,不用交换,因为自身交换自身,是没有必要的 mark++; if (i != mark) { int temp = arr[mark]; arr[mark] = arr[i]; arr[i] = temp; } } } //上面的循环完毕后,会自动的退出,那么得到的mark值就能直接的返回了 //那么这里可以不交换吗,答:不能 //因为单边的循环中,如果你没有操作交换,那么第一个必然是比其中一边都要大或者相同的,而由于必须小于第一个才会操作++,所以导致如果没有交换 //那么对应的mark的值不变,所以如果不交换的话,那么必然导致无限循环,并且你可能会认为,操作减一不就行了,但是如果操作减1,你怎么保证被减去的值是排好的呢 //所以在单边循环来说,必须操作交换 int temp = arr[mark]; arr[mark] = arr[pivot]; arr[pivot] = temp; return mark; } //他们都返回一个对应的下标,实际上该下标就是操作分开的 //这里给出为什么交换的主要原因,以及他是如何分开的 //实际上根据上面的参数就可以知道,他的分开只是将对应的起始下标和结尾下标改变而已,导致可以分开 //操作排序,即多次的操作排序 public static void quickSort(int[] arr, int start, int end) { //我们可以发现,他的一个操作包含了两个操作,像这种操作中,包含了自身的操作,一般都会使用递归,因为递归就是自身操作自身,在前面我们也通过循环来分解递归,比如使用对应的栈等等,前面的二叉树的遍历就是,但已经说过了,我只使用简单(方便)的操作,所以这里就使用递归了,因为代码量少,如果你需要循环的,自己去百度吧(虽然说百度,实际上只要是浏览器即可,只是我以前在电脑上使用百度比较多,所以习惯了,虽然我现在是使用谷歌浏览器的url地址来访问了,虽然一般默认会是百度访问) //定义结束条件 if (start >= end||arr==null) { return; //因为如果起始位置等于结尾位置,说明只有一个了,就不用操作了 } //这里以单边循环为例子,得到对应的重合下标 int mark = one(arr, start, end); //调用自身,即操作具体分开的数据,即如果没有操作交换,那么就是如下 quickSort(arr, start, mark - 1); //在单边循环中,这个减1可以不加(加上是为了减少判断),因为单边循环的操作,由于小的会在前面,大的会在后,所以就算操作自身也没有问题,且无论怎么分,都会导致局部的判断是小在前,大在后(因为反正分的时候,由于取已经分好的地方,再次的分,所以通常会导致只剩下3个或者2个时,必然可以会使得排序完毕,有些3个需要变成2个,比如3 1 2,即2个实际上才是必然排序完毕的) //所以我们可以感受到,就算不考虑数学的计算时间复杂度,那么由于他操作了分开的,就不用再次的判断左右两边的大小,所以很明显,就不用操作他们之间的联系了,而不是只会给出一个,所以比单纯的循环要好,因为单纯的循环,就算你已经分好了,也只能确定一个,而这里直接整体确定,且外加了基准这一个 //所以他的最差也是与循环一样,都是必须确定一个,但是他这里还有整体的确定,所以的确比循环要好 quickSort(arr, mark + 1, end); //因为是自身调用自身,所以对于其自身来说,给出的范围就相当于他们的start和end //由于线程的原因,必然需要等待前面一个操作完毕,才可操作后面一个,在以后说明并发编程时,会说明一个好的方式,也就是ForkJoinPool类,这里了解并注意即可 } //操作双边循环 public static void quickSort1(int[] arr, int start, int end) { //定义结束条件 if (start >= end||arr==null) { return; //因为如果起始位置等于结尾位置,说明只有一个了,就不用操作了 } //这里以单边循环为例子,得到对应的重合下标 int pivot = two(arr, start, end); //调用自身,如果没有操作交换,那么就是如下 quickSort1(arr, start, pivot - 1); //双边循环也可以不减去1,因为与单边一样,看单边的解释即可(因为交换的本质是一样的) quickSort1(arr, pivot + 1, end); } public static void main(String[] args) { int[] arr = new int[]{4, 7, 3, 5, 6, 2, 8, 1}; quickSort(arr, 0, arr.length - 1); System.out.println(Arrays.toString(arr)); arr = new int[]{4, 2, 3, 5, 6, 7, 8, 1}; quickSort1(arr, 0, arr.length - 1); System.out.println(Arrays.toString(arr)); //当然,相同怎么办,实际上并不需要理会,根据代码来说,相同的如果没有满足小于或者大于,那么一般都是认为是大的或者小的 //实际上他必然也是排序好的,因为对于自己来说,是相同,但是对于其他人来说就不是了 //当然,这里的数组不要是空的,否则arr.length - 1这里就会报空指针异常 } }
package com.lagou; import java.util.*; /** * */ public class test5 { //首先我们需要定义一个操作最大堆的方法,问题是,这个方法的作用是什么,看如下解释: //参数的意思是: //array:需要操作的数组 //start:要操作的起始下标(通常要是父节点) //last:要操作的结尾下标(对应父节点的后面的最终下标) //很明显,他是给数组进行操作范围来排序的,也就是说,给你这个子树来确定最大的值,即该子树变成最大堆 public static void max(int[] array, int start, int last) { //首先需要得到他的左右节点,先得到左节点 int chi = 2 * start + 1; //这里我们就定义一个数组来表示二叉树了,就不创建二叉树了 while (chi <= last) { //如果他在数组里面,那么说明,有这个节点存在 if (chi + 1 <= last && array[chi] < array[chi + 1]) { //到这里,说明左节点比,右节点小,那么就以右节点为主 chi++; } if (array[start] > array[chi]) { //如果父节点最大,那么直接的退出即可 break; //那么有个问题,前面的以右节点为主,以及这个的退出,难道不看没有操作的子节点的子节点是否有大于父节点的吗 //答:有这个问题是正确的,实际上,我们在操作时,是认为他是最大堆的 //也就是说,如果比他的左右节点大,那么必然比他的左右节点的子节点要大,在后面会给出具体的说明的 } //然后操作交换 int temp = array[start]; array[start] = array[chi]; array[chi] = temp; start = chi; //改变父节点 //实际上我们可以发现,他移动的值是贯彻所有的,所以,最终都会赋值该原来的父节点,所以上面可以先不进行交换,我们可以先保存原来的父节点即可 //当最后退出时,就进行赋值,当然,这样也能节省代码,所以这里可以进行改变 //然后继续以对应交换的地方进行操作,因为交换的地方,会改变堆的 chi = 2 * chi + 1; //很明显,上面他只会操作对应操作交换的节点,所以前面我会说成x-1 } } //我们以及定义好的堆的操作,现在,我们来操作排序,在排序中,来解决得到最大堆的操作 public static void sort(int[] array) { //将该数组变成最大堆 //我们首先先传递对应的最后一个非叶子节点,那么必然他会操作交换然后退出,然后我们持续的将他减减即可,因为如果没有对应的子节点,那么必然,会超过数组长度,所以会退出 //但由于他是完全二叉树,所以该退出的次数是基本没有的,否则,他又怎么出来呢,你说是吧(具体看完全二叉树的定义) for (int i = array.length / 2 - 1; i >= 0; i--) { max(array, i, array.length - 1); } //这样,就能每一个子树都操作了最大值,所以最终得到了最大堆,所以这就是前面的那个说明,因为我们是从左到右,从下到上的操作的 //所以保证了后面都是最大堆,所以方法里,只需要交换即可,而不用考虑 //接下来我们来操作最大的数,进行排序 for (int i = 0; i < array.length - 1; i++) { //得到原来数组最后一个数 int temp = array[array.length - 1 - i]; array[array.length - 1 - i] = array[0]; array[0] = temp; //然后再次的操作堆 max(array, 0, array.length - 1 - 1 - i); //由于后面都是最大堆,所以方法里面只需要交换即可 } } //上面我说过,可以不操作交换,具体代码如下: public static void max1(int[] array, int start, int last) { //先保存好原来的值,会贯彻所有的 int temp = array[start]; int chi = 2 * start + 1; while (chi <= last) { //如果他在数组里面,那么说明,有这个节点存在 if (chi + 1 <= last && array[chi] < array[chi + 1]) { chi++; } //始终以原来的数据进行比对,因为交换也是如此 if (temp > array[chi]) { break; } //然后操作交换 array[start] = array[chi]; //直接赋值即可 start = chi; //保存下一个父节点下标,因为以后赋值都是给他的 chi = 2 * chi + 1; } //如果退出了,说明要么原来的节点最大,要么没有子节点了,那么将到达的那个下标赋值 array[start] = temp; } public static void sort2(int[] array) { for (int i = array.length / 2 - 1; i >= 0; i--) { max(array, i, array.length - 1); } //这个for循环也进行改变 for (int i = array.length - 1; i > 0; i--) { int temp = array[i]; array[i] = array[0]; array[0] = temp; max(array, 0, i - 1); } } //这里为了解决之前说过的量数组变成二叉树,所以这里定义一个节点类 public class Node { int key; Node left; Node right; public Node(int key) { this.key = key; } } //保存头节点 Node root; //将数组变成二叉树 public void head(int[] array) { if (array != null) { List<Node> list = new ArrayList<>(); //创建相同数目的节点 for (int i = 0; i < array.length; i++) { list.add(new Node(0)); //先默认为0 } //进行连接 //首先赋值首节点 list.get(0).key = array[0]; root = list.get(0); for (int i = 0; i < list.size(); i++) { int left = 2 * i + 1; int right = 2 * i + 2; if (left < list.size()) { list.get(i).left = list.get(left); //当没有对应的下标时,就不会进行赋值,那么自然就是null了 list.get(left).key = array[left]; } if (right < list.size()) { list.get(i).right = list.get(right); list.get(right).key = array[right]; } } //操作打印: //使用广度优先遍历,这样,可以很明显的看出是否与数组保持一致,广度优先遍历前面已经说明过了 int[] result = new int[list.size()]; int i = 0; Queue<Node> queue = new LinkedList(); queue.offer(root); while (!queue.isEmpty()) { Node poll = queue.poll(); result[i] = poll.key; i++; if (poll.left != null) { queue.offer(poll.left); } if (poll.right != null) { queue.offer(poll.right); } } String b = ""; System.out.print("["); for (int ii = 0; ii < result.length; ii++) { if (ii == result.length - 1) { b = b + result[ii] + "]"; } else { b = b + result[ii] + ","; } } System.out.println(b); } else { System.out.println("没有数组信息"); } } //上面具体操作已经编写完成,现在我们来进行测试 public static void main(String[] args) { int[] arr = {7, 6, 4, 3, 5, 2, 10, 9, 8}; System.out.println("排序前:" + Arrays.toString(arr)); sort(arr); System.out.println("排序后:" + Arrays.toString(arr)); arr = new int[]{7, 6, 4, 3, 5, 2, 10, 9, 8}; System.out.println("排序前:" + Arrays.toString(arr)); sort2(arr); System.out.println("排序后:" + Arrays.toString(arr)); test5 t = new test5(); //打印对应的节点,如果他的值与数组基本对应(一样),说明节点连接成功 //当然,里面也使用了二叉树变成数组的形式,所以实际上数组和二叉树之间,已经都完成了,所以广度优先遍历本质上就是二叉树变成数组的形式 t.head(arr); //输出节点 System.out.println(t.root); } }
package com.lagou; /** * */ public class test6 { //计数排序的算法比较简单,有点类似于记录,在前面操作二分查找中,记录出现1次的那个使用hash的解释,就是记录的,与这个计数排序类似,只是他是哈希值来确定下标,而这里是值来确定下标(在那里也说明了这个) public static void Sort(int[] array, int offset) { //在前面的解释中,我们说明了偏移量,现在开始使用 int[] a = new int[array.length]; for (int i = 0; i < array.length; i++) { a[array[i] - offset]++; } //进行打印 System.out.print("["); int ii = 0; for (int i = 0; i < a.length; i++) { while (a[i] != 0) { if (ii == 0) { System.out.print(i + offset); ii++; a[i]--; } else { System.out.print("," + (i + offset)); a[i]--; } } } System.out.println("]"); } public static void main(String[] args) { int[] scores = {95, 94, 91, 98, 99, 90, 99, 93, 91, 92}; Sort(scores, 90); } }
package com.lagou; import java.util.*; /** * */ public class test7 { //定义方法来完成桶 public static void Sort(double[] array) { //先得到最大值和最小值,来确定桶的范围和数量 double max = array[0]; double min = array[0]; for (int i = 1; i < array.length; i++) { if (array[i] > max) { max = array[i]; } if (array[i] < min) { min = array[i]; } //这里有个问题,我们可以发现,如果对应的值等于max以及min,那么是怎样的呢,实际上既然等于,那么无论是max还是min,他的意思还是一样的,没有变,即max和min,即不用管 } //得到他们之间的差 double d = max - min; //因为桶里面包含多个数据,我们可以将这些数据用链表保存,那么保存链表,使用集合 //为什么不使用数组呢,也可以,只是非常的麻烦,特别是下标(放入元素时,需要判断是否是同一个下标,并放入对应下标,那么需要将变量放在外面了,这样还是不够的,还需要为每个桶定义下标位置,实在麻烦,这里就不做说明了),且使用链表不需要考虑扩,所以使用链表容 List<LinkedList<Double>> l = new ArrayList<>(array.length); //使用构造方法,固定list集合的大小,使得不会从10开始扩容 //而是该大小开始进行扩容(一般是1.5倍,这是list集合的底层原理,假设是15,那么就是增加7,因为15操作>>1就是7,或者说,是一半,只是7.5是没有的,偏向于下方,因为不满1是不会进1的,所以0.5就是0) //创建桶 for (int i = 0; i < array.length; i++) { //如果需要操作少一个桶,那么后面需要操作判断,即这里就说明了可以不操作最后一个桶 l.add(new LinkedList()); } //将元素根据区间放入桶中 for (int i = 0; i < array.length; i++) { //根据公式,找到对应的下标 int num = (int) ((array[i] - min) * (array.length - 1) / d); //进行取整,使得是那个下标 //在前面我说过,这里必须要减1,因为我们也知道,他是在某个区间的桶比例,由于区间是少于数量的 //比如1-2,2-3,3-4,4-5,明明是1,2,3,4,5这5个数,但是区间只有四个,所以这里实际上桶数量的确要减去1,但是又由于最大的值,必然是占用一个桶的,即需要包含最后一个,而不是省略他,而不省略他,因为下标的原因,必然是需要一个桶的,所以虽然说,桶数量与数组数量相同,又何尝不是因为区间的问题或者说下标的问题,导致出现最后一个桶呢,当然,你可以设置最后一个桶进行去掉,只需要下面的代码即可,认为他这个唯一也在倒数第二个桶里面,综上所述,因为区间的问题,所以真正的区间就是桶数量减去1 //这里的注释:是留给前面少一个桶的,但是在解释中,我们也知道,要操作最大值,所以这里就注释了 // if(num==l.size()){ // System.out.println(1); // num--; // } l.get(num).add(array[i]); //将对应的数,放在对应的桶中 } //每个桶都进行排序 for (int i = 0; i < l.size(); i++) { //对集合元素进行排序(使用自然排序,一般是从小到大的,具体看对应添加的数据重写compareTo的方法) Collections.sort(l.get(i)); } //进行打印 double[] sortedArray = new double[array.length]; int index = 0; for (LinkedList<Double> list : l) { for (double element : list) { sortedArray[index] = element; index++; } } System.out.print("["); for (int i = 0; i < sortedArray.length; i++) { if (i == sortedArray.length - 1) { System.out.println(sortedArray[i] + "]"); } else { System.out.print(sortedArray[i] + ","); } } } public static void main(String[] args) { double[] array = {4.12, 6.421, 0.0023, 3.0, 2.123, 8.122, 4.12, 10.09}; Sort(array); System.out.println("最大值是:" + array[array.length - 1]); array = new double[]{4.12, 6.421, 0.0023, 3.0, 2.123, 8.122, 4.12, 10.09}; List<LinkedList<Double>> max = max(array); Array(max, array); //从这里可以看出,我们可以分开操作,那么就不用必须先排序,才可以得到最大值了,而可以先放好来直接得到最大值,所以在需求最大值时,比如单纯的不操作最后一个桶要方便许多,即时间复杂度要低 //这就是为什么,多使用一个桶的原因 } //上面基本是没有操作区间跨度,实际上因为具体程序的下标公式已经操作完成了 //那么怎么去掉最后一个桶呢,答:再操作下标中,只要设置,判断即可,前面已经有了 //那么为了进行方便取最大值,这里进行操作分开 public static List<LinkedList<Double>> max(double[] array) { double max = array[0]; double min = array[0]; for (int i = 1; i < array.length; i++) { if (array[i] > max) { max = array[i]; } if (array[i] < min) { min = array[i]; } } double d = max - min; List<LinkedList<Double>> l = new ArrayList<>(array.length); for (int i = 0; i < array.length; i++) { l.add(new LinkedList()); } //将元素根据区间放入桶中 for (int i = 0; i < array.length; i++) { int num = (int) ((array[i] - min) * (array.length - 1) / d); l.get(num).add(array[i]); } System.out.println("最大值是:" + l.get(l.size() - 1).get(0)); return l; } public static void Array(List<LinkedList<Double>> l, double[] array) { //每个桶都进行排序 for (int i = 0; i < l.size(); i++) { //对集合元素进行排序(使用自然排序,一般是从小到大的,具体看对应添加的数据重写compareTo的方法) Collections.sort(l.get(i)); } //进行打印 double[] sortedArray = new double[array.length]; int index = 0; for (LinkedList<Double> list : l) { for (double element : list) { sortedArray[index] = element; index++; } } System.out.print("["); for (int i = 0; i < sortedArray.length; i++) { if (i == sortedArray.length - 1) { System.out.println(sortedArray[i] + "]"); } else { System.out.print(sortedArray[i] + ","); } } } }
package com.lagou;
/**
*
*/
public class test11 {
public static void main(String[] args) {
System.out.println("hello".indexOf("el")); //1
//即第一个e的位置,或者说,从下标1(字符串的下标)开始找到的
//int indexOf(int ch),用于返回当前字符串中参数ch指定的字符第一次出现的下标
}
}
package com.lagou; /** * */ public class test22 { public static void main(String[] args) { String i = "hello"; String k = "ll"; for (int j = 0; j < i.length() - k.length() + 1; j++) { //循环就是来操作次数的,这里也刚好是对应的次数,那么既然是次数,如果j是<=,那么后面的+1可以去掉,这是程序里的,而不是数学里面的,程序满足数学即可 //那么怎么匹配呢,很明显,我们需要取出对应的主串下标的对应值 //String substring(int beginIndex, int endIndex) //返回字符串中从下标beginIndex(包括)开始到endIndex(不包括)结束的子字符串 boolean equals = i.substring(j, k.length() + j).equals(k); if (equals == true) { System.out.println("下标在" + j + "位置找到"); return; } } System.out.println("没有找到"); } }
package com.lagou; /** * */ public class test33 { //首先我们先考虑26进制的计算方式,这里进行编写方法 public static int strToHash(String s) { int hash = 0; for (int i = 0; i < s.length(); i++) { hash *= 26; //为什么放在前面,因为位数的问题,所以放在前面,可以自己选择思考就知道了,假设是abc,那么a应该是26^2,而放在前面就可以解决 hash += (s.charAt(i) - 97); //减97节省计算,操作运算时,会字符会变成对应的int类型的数字的 } return hash; } //我们的哈希值操作完毕,现在编写方法 public static void is(String s, String b) { //首先我们先拿取对应的字串(模式串,匹配串)哈希值 int i = strToHash(b); for (int j = 0; j < s.length() - b.length() + 1; j++) { if (strToHash(s.substring(j, b.length() + j)) == i) { //到这里,说明哈希值相等,那么就匹配到了 System.out.println("匹配成功,下标是:" + j); return; } } System.out.println("没有找到"); //实际上substring不能超过其下标上限,否则报错,但是由于j < s.length() - b.length() + 1;的原因,使得如果模式串大于主串,自然是不会操作循环的,也就是直接的退出,所以基本不会报错 } public static void main(String[] args) { is("avfg", "fg"); } }
package com.lagou; /** * */ public class test44 { //操作模式串的哈希保存 //参数1,要保存的模式串 //参数2:要保存的位置数组 public static void generateBC(String b, int[] dc) { //我们知道,如果对应坏字符不存在,需要是-1,所以需要将对应的数组都设置为-1 for (int i = 0; i < dc.length; i++) { dc[i] = -1; } //设置好-1后,我们需要进行操作对应的下标进行放入,即放入自身的ASCII的值当下标,使得唯一,然该下标位置放入自己的所在模式串的下标 for (int i = 0; i < b.length(); i++) { dc[b.charAt(i)] = i; //charAt虽然得到的是字符,但是在与int操作时(比如赋值或者计算等等),会变成对应的ASCII值的 } //这样我们就保存好了 } //要操作坏字符,首先我们定义一个方法 public static void bad(String a, String b) { //定义数组 int[] array = new int[256]; //设置256是保证字符可以保存其下标 generateBC(b, array); //这里要说明一下:二维数组的创建有点不对称,但是这是规定,所以不用理会 /* 因为int[] a = new int [256] int[][] a = new int[][256] 这个是错误的,所以我才说不对称,实际上二维数组不能这样的说明我们要将int[][]看成是整个名称,而数组长度只写在前面括号,那么就说的通了 这里还需要考虑一个问题: 如果是这样的例子,怎么操作 acvvfdvf vfdvf */ //很明显,他找的是d前面的v,但是由于我们是保存最后一个,所以如果相减,自然为-1,而-1会导致往前移动,那么又会坏字符,并且可以找到,且v为4 //那么4-4=0,可以发现又回来了,导致无限循环,因为对应的i才是为主,我们移动的只是i的值而已,所以这里需要考虑该问题 //即如果坏字符一开始就找到了,那么我们可以使用哈希值来直接的找到,否则,我们最好还是进行遍历(总不能再次的操作空间吧,后面会给出代码来操作),那么有个问题,在中间的坏字符会与前面的2次的那个地方是一样的吗,答:不是一样的 //因为他的坏字符后面的已经也操作了判断,所以导致实际上多出几次,比如这里就多出2次,也就是说,这里操作判断了5次,却只移动2次,所以在原来的基础上,本来是多一次的,但是却又多了两次,所以不一样 //那么总体来说是好还是坏呢: //那就要看实际情况了,如果模式串是10个,那么我可以使用1次来完成10次,少9次,但是他也有对应的9次会使得多9次,那么总体来说是相同的,所以我们认为,单纯的坏字符与普通的哈希对比是等价的(认为我们定义的哈希对比是O(1),否则是大大提高的,前面已经说明过了) //现在我们来操作循环,这里我们默认还是一样的次数,因为他最多也是这样的次数 for (int i = 0; i < a.length() - b.length() + 1; ) { int j = 0; //我们来看看对应是否有坏字符 for (j = b.length() - 1; j >= 0; j--) { if (a.charAt(j + i) != b.charAt(j)) { //如果是字符,直接的等于即可 break; //既然不会相等,那么说明没有匹配成功,因为有坏字符 //那么如果都退出呢,怎么在循环中进行退出呢,实际上这里我们可以将j定义在外面,那么可以退出了 //并且也可以再次的使用 } } if (j < 0) { //设置小于是防止特殊情况,虽然这里不会出现 System.out.println("匹配成功,下标是:" + i); return; } int ii = 0; if (j != b.length() - 1) { char c = a.charAt(j + i); //到这里说明坏字符不是最后一个,那么操作遍历,这里也包含了一种特殊的情况,如果没有前一个(也就是在坏字符中没有坏字符前面的字符),也就是说,j-1中j是0,那么他会直接的退出,并且ii=-1.所以也会使得加上1(-(-1))的下标,也正好进行了移动 //所以这里也包括了直接退出的情况,所以就不用考虑直接退出的情况了 for (ii = j - 1; ii >= 0; ii--) { if (b.charAt(ii) == c) { break; } } } //这里就要考虑对应的坏字符是否存在的问题了,这里可以直接的计算,使用公式,也就是之前的si-xi //si是模式串坏字符对应下标,也就是j,xi是对应坏字符存在的下标,也就是array[a.charAt(j+i)] //即找到对应主串坏字符是否在数组中存在,而j+i代表在主串对应的坏字符的下标,得到该坏字符,我们使用他的ASCII去找数组的值来获得下标 //那么结果就是j-array[a.charAt(j+i)],就是移动的次数了 //因为有坏字符,那么我们进行对应的移动 if (j == b.length() - 1) { i += j - array[a.charAt(j + i)]; } else { //坏字符在中间 i += j - ii; } //移动后,自然也会进行重新操作的 } //如果都没有匹配成功,那么自然匹配失败 System.out.println("没有找到"); } //如果可以的话,实际上这里可以操作数组,只是赋值是反向赋值即可,并且找到相同的就不操作赋值,这样,后面的代码就不用判断了 //也就是上面注释说过的"总不能再次的操作空间吧,后面会给出代码来操作" //代码如下: public static void generateBC11(int[] dc) { for (int i = 0; i < dc.length; i++) { dc[i] = -1; } } public static void generateBC12(String b, int[] dc, char v) { for (int i = b.length() - 1; i >= 0; i--) { if (b.charAt(i) == v) { dc[v] = i; break; } } } public static void badd(String a, String b) { int[] array = new int[256]; generateBC11(array); for (int i = 0; i < a.length() - b.length() + 1; ) { int j = 0; for (j = b.length() - 1; j >= 0; j--) { if (a.charAt(j + i) != b.charAt(j)) { break; } } if (j < 0) { System.out.println("匹配成功,下标是:" + i); return; } String c = b.substring(0, j); generateBC12(c, array, a.charAt(j + i)); //通过其方法可以看到,实际上与遍历是一样的,所以说使用这种也行,次数也与遍历(包括先保存的)加上总遍历是基本是等价的(特殊的方法) //但是该情况又何尝不是没有使用哈希对比下标之前的坏字符吗,只是遍历使用数组保存而已,所以总体来说 //我们会使用第一种情况(即先都保存,然后遍历,即上面的bad方法,badd是第二种情况,没有操作哈希的,我们可以认为是第三种情况,虽然这里没有说明,因为总不能所有情况都写上吧,所以从这里开始,我们只会选择主要的情况来操作,并且也选择方便操作来操作了) //这两种你可以随便使用其中一种,这里我们就操作第一种,因为第一种在坏字符是最后的情况下,执行的代码是少的,其他时候,基本差不多,所以第一种好丢丢 i += j - array[a.charAt(j + i)]; if (i + j <= a.length() - 1) { array[a.charAt(j + i)] = -1; //重新变回原来的样子,否则会使得没有赋值退出时,认为还存在,特别是截取的字符串是""的时候 } } System.out.println("没有找到"); } //我们可以看到,我们操作的基本的坏字符查询实际上并没有什么明显的提升,因为是等价的,在有些情况下可以一下子找到,有些情况下难找一点,总体等价 //现在我们来进行扩展使得加上好后缀 public static void bad1(String a, String b) { int[] array = new int[256]; generateBC(b, array); //加上好后缀 //因为好后缀的原因,那么他必然会使得坏字符到中间,我们继续看后面代码的解释 for (int i = 0; i < a.length() - b.length() + 1; ) { //后面需要加上;,否则报错,这是java的结束符,不要忘记了 int j = 0; for (j = b.length() - 1; j >= 0; j--) { if (a.charAt(j + i) != b.charAt(j)) { break; } } if (j < 0) { System.out.println("匹配成功,下标是:" + i); return; } int ii = 0; if (j != b.length() - 1) { char c = a.charAt(j + i); //到这里说明坏字符不是最后一个,那么操作遍历 for (ii = j - 1; ii >= 0; ii--) { if (b.charAt(ii) == c) { break; } } } //接着上面的问题,我们先操作如下: //这里要考虑移动谁了 //首先我们得到坏字符的移动次数 int aa; boolean is = true; if (j == b.length() - 1) { aa = j - array[a.charAt(j + i)]; is = false; //代表,没有好后缀 } else { //坏字符在中间 aa = j - ii; } //然后得到好后缀的移动次数 //但是好后缀的移动次数在前面并没有说明,现在我们来思考一下,公式是什么 //首先给出例子: /* acvvfdv vfdvf */ //我们可以知道,后面两个vf是相等的,那么通过上面的程序,应该找到前面的vf //很明显,这里需要移动3次,而坏字符移动2次,所以我们应该是移动3次 //但是我们需要考虑过度滑动的情况 //但首先,我们先不考虑过度的滑动,我们先将该3次进行计算出来,我们可以很明显的发现,他移动的次数,是其中最后一个v下标减去前一个v下标,也就是3-0=3 //但是前一个v需要保证其后面的与原来匹配好的好后缀一致,所以这里要注意,后面会有举例 //那么有个问题,我们需要考虑是否存在好后缀呢,答,要考虑 /* 举例: dfgh fg 如果我们不考虑好后缀是否存在,那么由于g只会存在一个,考虑普通的移动,也就使得1-(-1)=2了,那么就跳过了正确的匹配(结合代码,要移动两次),当然真正的这里的代码,即在下面的代码中,会导致越界 比如: for (int kk = j - (b.length()-1 - j - 1); kk >= 0; kk--) { //判断算法相同的起点 if (b.charAt(kk) == b.charAt(j + 1)) { 这个b.charAt(kk) 会越界,因为上面的kk比原来的j要大了,导致越界了 所以需要进行判断是否存在好后缀 */ //这里我们直接以坏字符为主即可(后面的if中的>=) /* 同样的,他这里也只会保存后两个字符,所以如果出现这种情况也是不行的: acvvfvvfdvfv vfdvfv 即坏字符在模式串的d中,由于坏字符我们已经操作过了,他不会有问题,但是好后缀怎么办呢,很明显,这里应该要跳过,也就是说,在保证找到第一个v时,也要保证后面相同 */ //现在我们来操作 //我们如何确定好后缀的下标呢,实际上j+1就是对应的好后缀的起始的下标,前提是,怎么操作第二个整体好后缀(比如举例的dgg) //那么只能操作循环了 //次数是: /* 举例: wertddfdggsas weftdggdgg 很明显,我们应该需要从坏字符那里的g的前面一个d开始,那么很明显,就是坏字符下标减去2,而这个2,就是模式串的最大下标减去坏字符下标再减1 */ int bb = 0; if (is == true) { //定义找到的下标,如果没有找到,那么默认为模式串的长度 int gg = b.length(); for (int kk = j - (b.length() - 1 - j - 1); kk >= 0; kk--) { //判断算法相同的起点 if (b.charAt(kk) == b.charAt(j + 1)) { //定义退出变量 int oo = 0; //现在我们开始判断后面的是否相同,但在之前,首先看看有没有后面的字符 //如果下面的循环直接的退出,自然就代表没有后面的字符 for (int pp = 1; pp < b.length() - 1 - j - 1; pp++) { //根据例子来说,只需要对应的两次即可 if (b.charAt(pp) != b.charAt(j + 1 + pp)) { oo = 1; break; } } if (oo == 0) { gg = kk; } } } int p = gg; //这里来操作一下过度滑动的判断,我们只需要看看最前两个中(上面的例子),是否在匹配好的中有对应相同的字符 if (gg == b.length()) { int tt = 0; //这里也是b.length() - 1 - j-1,因为起始的都没有匹配,那么就不需要匹配了 for (int ll = b.length() - 1 - j - 1, jj = 1; ll > 0; ll--, jj++) { //执行对应次数 if (b.charAt(0) == b.charAt(j + 1 + ll)) { tt = jj; } } gg -= tt; } if (p == b.length()) { bb = gg; } else { bb = j + 1 - gg; } } if (aa >= bb) { i += aa; } else { i += bb; } } System.out.println("没有找到"); } //我们可以看到,使用好后缀,使用了非常多的代码,判断了很多次,在前面,坏字符操作中间时,他多出了对应的好后缀的次数,而这里在该次数上,一般也多出了很多次,即原来是等价的,但是现在多了对应的次数了,该次数最少是好后缀减1(因为过度),最多是前面第一个判断是否有相同后缀的好后缀,即:(好后缀/总好后缀)*(模式串长度),由于出现的起始数量是随机的,那么根据大致平均,我们可以认为他与坏字符等价(包括前面多余的2次好后缀以及过度),即我们可以认为,在坏字符的基础次数上,认为加上和坏字符的次数即可,即原来的等价,现在加上了同样的等价 //也就是说,该BM算法(好后缀)很极端,慢的很慢,快的很快,虽然坏字符也是如此,所以虽然在一般小的模式字符串使用BM算法好,即他的慢自然也是O(n+m),因为在等价的基础上,加上了对应模式串的次数 //这里我们以快的为准,如果是最快的,那么很明显,就是一次性得到,这里我们忽略一开始的创建数组(因为数组是可以被很多人利用的,所以我们可以忽略) //即空间换取时间 //那么我们以坏字符为例,如果始终是最后一个是坏的字符,那么就是n/m了,而好后缀可以使得,就算是中间的坏字符,那么若始终没有其他的好后缀的后面字符,那么也可以是n/m,虽然他可能会提高次数,但是,只是最高的水平,而纵观全局,实际上要提高很多次数的(坏字符也是,他们是等价的),即若在极端的情况下,因为每次都是移动模式串的大小,即这是最快的情况,所以在极端情况下,我们一般会称BM的时间复杂度是O(n/m) //否则平常我们可能会认为是O(n+m),因为等价下,又会加上好后缀的次数(即m,虽然是等价,但是并非是相同的,即等价只是时间复杂度是类似的而已) //并且,好后缀和坏字符的程序,如果没有好的优化的话,建议不要结合来使用,因为虽然极端情况下,会使得找的很快,但大多数是慢的,因为他们的时间复杂度是相加的(除非你找的字符很少,因为很少的模式串,极端情况容易出现,使得他们的判断可能因为其中一个而直接的跳过,比如坏字符在最后面,那么没有好后缀,那么好后缀就会跳过,在程序里已经操作了),但实际上好后缀和坏字符也能自己来操作,那么可以说,那么他们只需要他自身的长度的次数 //所以在不建议结合来使用,是因为有些情况好后缀好,有些情况坏字符好,比如dffggghhh,模式串是gff,那么坏字符只会移动一次,而好后缀直接移动3次 //但若模式串是vffggdh,那么坏字符直接移动6次,而好后缀却只移动1次,所以他们各有优势 //而结合的话,就没有优势了(因为都操作了对应代码,即时间复杂度相加了),因为没有人可以同时操作两个,所以这里就需要看使用者的选择了,就如数组和链表一样,总得选择一个吧,总不能都放在一起,都来使用或者操作吧,当然,这里给出了对应的结合代码,以及分开的代码,根据上面的介绍,所以我们尽量操作分开的 //至此,坏字符和好后缀说明完成,注意:这里是结合当前代码来分析的,如果理论不准确,或者时间复杂度不对,那么就是说明程序还可以优化 //现在以坏字符为主,看看正方向的操作是否不好 public static void bad2(String a, String b) { int[] array = new int[256]; generateBC(b, array); for (int i = 0; i < a.length() - b.length() + 1; ) { int j = 0; //要验证操作正向的,这里需要改变吗,答:这里不需要,因为他只是从最后开始进行判断是否相同的而已 for (j = b.length() - 1; j >= 0; j--) { if (a.charAt(j + i) != b.charAt(j)) { break; } } if (j < 0) { System.out.println("匹配成功,下标是:" + i); return; } //这里才是真正的验证正方向的地方 // int ii = 0; // if (j != b.length() - 1) { // char c = a.charAt(j + i); // // for (ii = j - 1; ii >= 0; ii--) { // if (b.charAt(ii) == c) { // break; // } // } // } int ii = 0; if (j != b.length() - 1) { char c = a.charAt(j + i); for (int iii = 0; iii <= j - 1; iii++) { if (b.charAt(iii) == c) { ii = iii; } } //需要判断没有操作循环的情况,也就是在坏字符中没有坏字符前面的字符(前面说明过了) if (j == 0) { ii = -1; } //当遍历结束后,那么自然得到的就是最终的下标,也就是ii,很明显,他需要都进行遍历,没有break,所以我们才说,正方向比反方向不好,因为正方向不能确定他是否是最后一个该字符,所以需要都进行循环 } if (j == b.length() - 1) { i += j - array[a.charAt(j + i)]; } else { i += j - ii; } } System.out.println("没有找到"); } //单纯的好后缀操作 public static void bad3(String a, String b) { int[] array = new int[256]; generateBC(b, array); for (int i = 0; i < a.length() - b.length() + 1; ) { int j = 0; for (j = b.length() - 1; j >= 0; j--) { if (a.charAt(j + i) != b.charAt(j)) { break; } } if (j < 0) { System.out.println("匹配成功,下标是:" + i); return; } boolean is = true; if (j == b.length() - 1) { is = false; } int bb = 0; if (is == true) { int gg = b.length(); for (int kk = j - (b.length() - 1 - j - 1); kk >= 0; kk--) { if (b.charAt(kk) == b.charAt(j + 1)) { int oo = 0; for (int pp = 1; pp < b.length() - 1 - j - 1; pp++) { if (b.charAt(pp) != b.charAt(j + 1 + pp)) { oo = 1; break; } } if (oo == 0) { gg = kk; } } } int p = gg; if (gg == b.length()) { int tt = 0; for (int ll = b.length() - 1 - j - 1, jj = 1; ll > 0; ll--, jj++) { if (b.charAt(0) == b.charAt(j + 1 + ll)) { tt = jj; } } gg -= tt; } if (p == b.length()) { bb = gg; } else { bb = j + 1 - gg; } } i += bb; } System.out.println("没有找到"); } public static void main(String[] args) { String a = "dffvff"; String b = "dvf"; System.out.println("下标从0开始的哦"); bad(a, b); badd(a, b); //上面的坏字符查询,结果基本正确 bad1(a, b); //在坏字符上,加上好后缀查询,结果基本也正确 bad2(a, b); //操作正向的找坏字符 bad3(a, b); //单纯的好后缀,只需要取出坏字符的代码即可,因为他们是没有必然关系的,只是用来判断而已 } }
public class TrieNode {
public char data; //保存对应的字符
public TrieNode[] children = new TrieNode[26]; //该节点拥有的变量,由于最大只有26,所以我们只需要定义26个长度即可,他就可以认为是对应的子节点
public boolean isEndingChar = false; //代表到这个节点是存在的意思
public TrieNode(char data) {
this.data = data;
}
}
package com.lagou; /** * */ public class test55 { //首先就是对应的类 public static class TrieNode { public char data; //保存对应的字符 public TrieNode[] children = new TrieNode[26]; //该节点拥有的变量,由于最大只有26,所以我们只需要定义26个长度即可,他就可以认为是对应的子节点 public boolean isEndingChar = false; //代表到这个节点是存在的意思 public TrieNode(char data) { this.data = data; } } //定义起始节点 public static TrieNode root = new TrieNode('/'); //默认无效字符 //创建插入节点(字符串) public static void insert(String a) { //我们先定义临时变量得到起始节点,不让他改变指向 TrieNode node = root; //现在我们来进行保存 for (int i = 0; i < a.length(); i++) { //我们先得到开始的一个 char c = a.charAt(i); //得到下标,由于是a-z的,那么我们减去97,来得到对应的数组下标 int i1 = c - 97; //如果对应的下标是null,那么添加上去 if (node.children[i1] == null) { node.children[i1] = new TrieNode(c); } node = node.children[i1]; } //从上面的循环来看,他始终对后面进行添加,当退出时,我们需要为最后一个定义标识,使得保存的该位置是一个字符串 node.isEndingChar = true; //因为,如果你不是该字符串,那么必然是不会是true的 } //查找字符串(节点) public static void select(String a) { //我们先定义临时变量得到起始节点,不让他改变指向 TrieNode node = root; //现在我们来进行查找 for (int i = 0; i < a.length(); i++) { //我们先得到开始的一个 char c = a.charAt(i); //得到下标,由于是a-z的,那么我们减去97,来得到对应的数组下标 int i1 = c - 97; //如果对应的下标是null,那么说明没有,直接退出 if (node.children[i1] == null) { System.out.println("没有该字符串"); return; } node = node.children[i1]; } if (node.isEndingChar == true) { //如果是true,说明是存在的 System.out.println("有该字符串"); } else { System.out.println("没有该字符串"); } } public static void main(String[] args) { insert("hello"); insert("her"); insert("hi"); insert("how"); insert("see"); insert("so"); select("how"); } }
package com.lagou; import java.util.ArrayList; import java.util.List; /** * */ public class test66 { //要操作直观的图,首先,我们先需要对应的变量,这里以二维数组内容进行说明(因为无论是有向图,还是无向图,还是带权图,都是二维数组的内容) //我们要保存对应的节点,自然需要类,而要保存很多节点,那么一般使用集合 public List list; //由于该节点没有上下级关系(如没有二叉树的子节点和父节点的关系),所以该集合只是保存节点而已 //节点保存好了,那么节点的信息是什么,我们知道,节点产生的信息,自然是需要其"边"的信息,以及"顶点"信息 //由于二维数组是都包含好了,所以实际上节点里面没有信息,且他本身就是对应的顶点信息 //所以,实际上节点没有信息,即节点只是用来确定"边"信息的 //我们知道,边的关系以二维数组来表示,代表有指向那个边(边节点),以及总度(边的指向数,即边的条数)数 //所以定义一个二维数组来保存边的关系 int[][] i; //他的长度,由我们自己来操作 //定义总边数(即总度,即所有的度的数量,而不是一个节点度的数量) int du; //从上面可以看出,我们的节点可以是Object,这里我们以String为主,看最后的操作代码就知道了 //当然,由于节点信息和其边的信息都在当前类里面,所以我们需要进行初始化 public test66(int n) { //这里传递参数:二维数组的长度,自然也是链表节点长度,那么就是如下: i = new int[n][n]; //因为对应图的二维数组是行和列是一样的,所以直接初始化即可 //刚刚加上的节点,那么对应的总边数(所有节点,如果是该节点,那么我会说成,该节点的总边数,或者该节点的度,而不是总边数或者总度),自然是0 du = 0; //在对应的链表中进行加上,来保存 list = new ArrayList<>(n); //至此,对应节点链表以及二维数组操作完成 } //现在,我们定义插入节点 public void insertNode(Object node) { list.add(node); //后面没有什么操作,那么很明显,实际上节点的数量是可以凭空产生的,即节点与边的关系只是逻辑上的关系而已,所以并不是有节点就一定要连接边 } //插入节点后,自然就是插入边 public void insertEdge(int a, int b, int c) { //我们知道,边是在二维数组里面的,所以说,插入的边就是给二维数组赋值,只是逻辑上记得要与节点对应 i[a][b] = c; //插入一个边,那么总度数,自然要加1 du++; } //定义返回权值,即二维数组对应的值 public int getWeight(int a, int b) { return i[a][b]; } //返回节点的数据,我们知道节点本身就是顶点,那么他的数据就是保存的节点信息 public Object getNode(int i) { //给出参数,是为了要查看哪(也可以说成"那",但通常是手滑打出的字)个节点 return list.get(i); } //得到边的总数量 public int getEage() { return du; } //得到节点的总数量 public int getNodeAll() { return list.size(); } //至此,边和节点的插入,以及节点边的总数量,以及对应边的权值以及对应节点的数据,这6个基本操作编写完成 //现在我们来进行测试 public static void main(String[] args) { //为了更好的测试,我们操作初始化的信息少点 //对应节点和边的数量 int a = 4; //当然,上面定义的是最大值,所以要注意 //定义节点的信息,这里就在前面说明了以String的信息为主,所以这里就是如下: String[] s = {"v1", "v2", "v3", "v4"}; //创建对象 test66 t = new test66(a); //插入节点 for (String c : s) { t.insertNode(c); } //现在我们进行插入边 t.insertEdge(0, 1, 2); //t.insertEdge(0, 1, 2);,因为是逻辑的,所以如果这里也加上的话,会使得对应的总边数加1 //t.insertEdge(1, 1, 2);,这个也会,但实际上却不能操作,因为没有自身指向自身的,可是这里的总边数还是会加1,所以说他是逻辑的 t.insertEdge(0, 2, 5); t.insertEdge(2, 3, 8); t.insertEdge(3, 0, 7); /* 这里我要说明一下,二维数组的表示图形显示: 你可以这样(第一种) v1 v2 v3 v4 v1 0 2 5 0 v2 0 0 0 0 v3 0 0 0 8 v4 7 0 0 0 很明显,每一行代表二维数组中的i[a],而不是i[a][b]的[b] 你也可以这样(第二种) v1 v2 v3 v4 v1 0 0 0 0 v2 2 0 0 0 v3 5 0 0 7 v4 0 0 8 0 很明显,每一列代表二维数组中的i[a],而不是i[a][b]的[b] 这都是不同的表示形式,至于使用那一种,看你自己,因为他们都可以 只是一般情况下,第一种好点,因为行看起来比较整齐,且我们通常喜欢看行,然后是列,当然主要看自己的习惯 */ //这里以第一种为主 //我们来观察他的信息,v1这个节点,他指向了v2和v3(逻辑上的),v3指向v4,v4指向v1,对应的值就是权值 //至此,该基本图操作完毕 System.out.println("结点个数是:" + t.getNodeAll()); System.out.println("边的个数是:" + t.getEage()); } }
package com.lagou; import java.util.HashMap; import java.util.Iterator; import java.util.Map; import java.util.Set; /** * */ public class test77 { //定义顶点类 public class Vertex { String name; //顶点名称,也就是之前的顶点的值,只是这里是节点了,我们保存该名称即可 Edge next; public Vertex(String name, Edge next) { this.name = name; this.next = next; } } //定义边类(也就是指向的顶点),这里之所以看成是边类,是因为,好像这里的内容就是边的信息,所以我们一般也会将该类称为边,内容称为指向顶点的内容(包含顶点) public class Edge { String name; //被指向的顶点,或者说,就是顶点,所以可以认为这里定义的是边的信息 int weight; //对应的权重(权,权值) Edge next; //指向的下一个边 public Edge(String name, int weight, Edge next) { this.name = name; this.weight = weight; this.next = next; } //之所以这样,是因为数组有很多顶点,那么其链表只需要保存边即可,所以因为数组保存顶点,链表保存边,即我们也会称为分开的保存 } //类已经定义好了,那么我们来进行操作,即上面我们定义了顶点,以及边(指向了顶点),即他们的类信息 //在前面我们操作时,是以集合保存顶点的,现在,为了不操作二维数组,所以我们来进行直接使用链表,当然,之前的集合本质上也是数组,所以这里本质上就是数组加链表 //我们先定义数组,当然,该数组我们可以使用集合,这里考虑使用map集合的数组 Map<String, Vertex> map; //前面的String也用来保存顶点名称的,这样,就不用我们来考虑下标来得到名称了,使得可以对应,这就是使用map集合的原因 //这里就不考虑对应的顶点总边了(当然,考虑所有顶点总边也行),因为我们选择主要的操作来进行操作的(前面在说明过了,即在BM算法代码中有说明"我们只会选择主要的情况来操作") //我们进行初始化 public test77() { map = new HashMap<>(); } //首先我们需要进行两个主要操作,即添加顶点以及添加边(包含顶点) //添加顶点 public void insertNode(String a) { //参数自然就是顶点的名称 Vertex aa = new Vertex(a, null); //一开始是没有边的 map.put(a, aa); } //添加边,我们需要参数如下:被指向顶点的名称,权值,给哪个对应的顶点 public void insertEdge(String a, String b, int c) { //其中a代表以及添加的顶点名称,b代表被指向的顶点名称,c代表权值 //那么这里为了先考虑若没有对应的顶点怎么办,所以我们操作补偿 Vertex vertex = map.get(a); if (vertex == null) { vertex = new Vertex(a, null); //一开始是没有边的 map.put(a, vertex); //之所以这样,是为了后面可以再次的得到(因为指向改变了) } //如果没有顶点,自然我们手动的创建 //现在我们来添加边 if (vertex.next == null) { vertex.next = new Edge(b, c, null); } else { Edge temp = vertex.next; while (true) { if (temp.next == null) { //到这里代表没有边,那么添加 temp.next = new Edge(b, c, null); return; } temp = temp.next; } } } //上面我们编写完毕,但为了更好的操作遍历,我们可以写一个遍历方法 public void Select() { //我们知道他是map集合,他的总体打印一般需要先进行转换(是他的操作,而不是我之前手写的map集合,因为我有对应的方法) Set<Map.Entry<String, Vertex>> entries = map.entrySet(); Iterator<Map.Entry<String, Vertex>> iterator = entries.iterator(); while (iterator.hasNext()) { Map.Entry<String, Vertex> entry = iterator.next(); Vertex vertex = entry.getValue(); //得到其中数组的顶点 Edge edge = vertex.next; //得到该顶点的下一个边类 while (edge != null) { System.out.println(vertex.name + " 指向 " + edge.name + " 权值为:" + edge.weight); edge = edge.next; } } } public static void main(String[] args) { test77 graph = new test77(); graph.insertNode("A"); graph.insertNode("B"); graph.insertNode("C"); graph.insertNode("D"); graph.insertNode("E"); graph.insertNode("F"); //给c这个顶点,添加A这个顶点,对应的权值是1,后面依次类推即可 graph.insertEdge("C", "A", 1); graph.insertEdge("F", "C", 2); graph.insertEdge("A", "B", 4); graph.insertEdge("E", "B", 2); graph.insertEdge("A", "D", 5); //可以与graph.insertEdge("A", "B", 4);交换位置,可以发现打印顺序从原来的先输出B,变成了先输出D graph.insertEdge("D", "F", 4); graph.insertEdge("D", "E", 3); graph.Select(); } //当然,上面的添加也是逻辑性的,虽然他们可以任意的添加,但我们可以发现,边的添加可以是相同的,这是不好的情况,而顶点,因为map集合的原因,相同的则会覆盖(会使得没有边,因为方法的缘故),当然,这些问题是细节问题,这里没有理会,你可以选择进行操作修改 //这里实际上也与二维数组一样的,都是逻辑上的连接 //因为实际上图就是逻辑结构 }
package com.lagou; import java.util.*; /** * */ public class test88 { //这里以有向图来进行操作,顺便也结合带权(带权图) //我们这里还是使用链表加数组来表示,所以有些代码利用之前的 public class Vertex { String name; Edge next; boolean b = false; //默认没有访问,注意:边不需要进行设置该变量,因为其不是唯一,这里代表唯一的,边只是定义其指向信息的,而不能是顶点 //所以这里更加的说明一下,边就是边,而不是顶点 public Vertex(String name, Edge next) { this.name = name; this.next = next; } } public class Edge { String name; int weight; Edge next; public Edge(String name, int weight, Edge next) { this.name = name; this.weight = weight; this.next = next; } } //注意:因为他们是不同的类,我存放在栈里面,一般直接存放其名称即可,因为我们可以通过名称来得到其节点(虽然他们是分开的) //当然,你可能会问,设置相同的类不就好了,实际上也行,只是总不能所有的类都有权重(权值吧),因为一开始是没有边的,你可以再编写测试后,将Edge类删除,然后在Vertex类里加上权重变量,可以发现,虽然添加节点,需要添加权重,其他的代码只需要改变将Edge变成Vertex即可,结果还是一样的,因为其操作的内容还是一样,只是连接的是当前类而已,但是一开始怎么能够有权重呢 //所以如果你不操作权重,那么可以是相同的类(虽然就算操作了也行),因为我们只要指向就行,这里为了完美操作,我们就加上权重了,并且定义两个类,来具体区分,总不能又当顶点,也当边吧,要明确的分工,虽然需要多定义一个类 Map<String, Vertex> map; public test88() { map = new HashMap<>(); } public void insertNode(String a) { //这里我们进行修改 if (map.get(a) == null) { Vertex aa = new Vertex(a, null); map.put(a, aa); } //总不能使得操作覆盖,使得没有边了吧 } public void insertEdge(String a, String b, int c) { Vertex vertex = map.get(a); if (vertex == null) { vertex = new Vertex(a, null); map.put(a, vertex); } if (vertex.next == null) { vertex.next = new Edge(b, c, null); } else { Edge temp = vertex.next; Edge temp2 = vertex.next; while (true) { //为了后续队列的操作,这里需要进行大小的判断,由于不用考虑头,所以代码相对简单 //防止添加相同的边 if (temp.name == b) { temp.weight = c; return; } if (temp.name.hashCode() > b.hashCode()) { //那么就说明新添加的比较小,所以对应的要放在前面 if (temp2 != vertex.next) { //进入这里,说明他已经经历了一个循环 Edge j = new Edge(b, c, null); temp2.next = j; j.next = temp; } else { //进入这里,说明就是第一次 Edge j = new Edge(b, c, null); vertex.next = j; j.next = temp2; } return; } if (temp.next == null) { temp.next = new Edge(b, c, null); return; } //其他情况就是大的,当然,这里可以不用判断,因为外面就是大的了,虽然也可以判断,之前的链表操作就是判断的,这里就不判断了,反正是一样的 temp2 = temp; temp = temp.next; } } } public void Select() { Set<Map.Entry<String, Vertex>> entries = map.entrySet(); Iterator<Map.Entry<String, Vertex>> iterator = entries.iterator(); while (iterator.hasNext()) { Map.Entry<String, Vertex> entry = iterator.next(); Vertex vertex = entry.getValue(); Edge edge = vertex.next; while (edge != null) { System.out.println(vertex.name + " 指向 " + edge.name + " 权值为:" + edge.weight); edge = edge.next; } } } //当然,对应的有向图是逻辑的,所以主要看对应的添加信息的方式(后面的添加是有向图的),所以这里我们直接的编写广度优先搜索和深度优先搜索即可 //深度优先搜索 public void Select1(String a) { //要完成深度优先搜索,我们可以先选择看之前的图片,也就是"针对上面邻接矩阵比较浪费内存空间的问题,我们来看另外一种图的存储方法,邻接表(Adjacency List)"这段话下面的图片 //我们操作的就是他,所以我们添加的是有向图 //那么由于我们需要一个起点,所以这个参数a就是起点的节点名称,当然,后面会给出一个终点的操作 //我们既然选择该起点,那么就需要将他进行标记,前提,首先需要都进行解除,因为,当多次的执行,那么标记总不能重复使用吧 //初始化标记 Set<String> strings = map.keySet(); Iterator<String> iterator = strings.iterator(); while (iterator.hasNext()) { String next = iterator.next(); map.get(next).b = false; } //初始化完成后,现在,我们进行深度遍历 //定义输出的数组信息 String[] array = new String[map.size()]; //定义起始下标 int i = 0; //首先得到该节点 Vertex vertex = map.get(a); //然后输出信息(放在数组里面) array[i] = vertex.name; i++; //定义一个栈 Stack<Object> stack = new Stack<>(); stack.push(vertex.name); //入栈 vertex.b = true; //设置标记 //然后看看该节点对应的边的内容节点最先是谁,由于对应都是由字母进行设置的,所以实际上可以使用他们本身的哈希值来决定先后 while (true) { //定义最先内容 String na = null; Edge edge = vertex.next; if (edge != null) { //只有,有下一个的,才会操作这里的操作 while (edge != null) { //因为添加的原因,所以其链表第一个或者前一个就是最小的 if (map.get(edge.name).b == false) { na = edge.name; //找到最小就不用在循环了 break; } edge = edge.next; } //若代表有值则为true,否则都是标记的,那么也直接的出栈 if (na != null) { //上面得到了最先的顺序,那么我们加入栈 stack.push(na); array[i] = na; i++; map.get(na).b = true; //设置标记 } else { //都标记了,那么直接的出栈 stack.pop(); } } else { //没有下一个了,那么直接的出栈 stack.pop(); } //如果没有栈内容了,那么说明都出栈了,即都遍历完毕了 if (stack.isEmpty()) { break; } //获得当前顶点 String peek = (String) stack.peek(); //改变顶点 vertex = map.get(peek); } //打印遍历的信息 System.out.print("["); for (int ii = 0; ii < array.length; ii++) { if (ii == array.length - 1) { System.out.print(array[ii]); } else { System.out.print(array[ii] + ","); } } System.out.println("]"); } //至此,深度优先搜索操作完毕 //现在我们来编写广度优先搜索 public void Select2(String a) { //前面几乎与深度优先搜索一样 Set<String> strings = map.keySet(); Iterator<String> iterator = strings.iterator(); while (iterator.hasNext()) { String next = iterator.next(); map.get(next).b = false; } //初始化完成后,现在,我们进行广度遍历 //定义输出的数组信息 String[] array = new String[map.size()]; //定义起始下标 int i = 0; //首先得到该节点 Vertex vertex = map.get(a); //定义一个队列 Queue<String> queue = new LinkedList<>(); queue.offer(vertex.name); //入队 vertex.b = true; //设置标记 //出队 String poll = queue.poll(); array[i] = poll; i++; while (true) { //操作入队 Edge edge = vertex.next; if (edge != null) { while (edge != null) { //这里我们需要有顺序的进行入队,所以我们需要将链表进行排列,当然,这个操作,我们需要在添加边中进行 //前面的添加操作中有说明了,他是小的在前面,大的在后面,所以这里直接的入队即可 if (map.get(edge.name).b == false) { queue.offer(edge.name); map.get(edge.name).b = true; //标记 } edge = edge.next; } } //最后一个出队也到这里,说明操作完毕 if (queue.isEmpty()) { break; } //上面将相关的都入队后,然后外面操作出队,若没有入队的,那么自然是对应下一个出队 String poll1 = queue.poll(); array[i] = poll1; i++; vertex = map.get(poll1); //得到出队的顶点 edge = vertex.next; //操作出队顶点的入队 } //打印遍历的信息 System.out.print("["); for (int ii = 0; ii < array.length; ii++) { if (ii == array.length - 1) { System.out.print(array[ii]); } else { System.out.print(array[ii] + ","); } } System.out.println("]"); } //至此,广度优先遍历操作完毕 public static void main(String[] args) { test88 graph = new test88(); graph.insertNode("A"); graph.insertNode("B"); graph.insertNode("C"); graph.insertNode("D"); graph.insertNode("E"); graph.insertNode("F"); //给c这个顶点,添加A这个顶点,对应的权值是1,后面依次类推即可 graph.insertEdge("C", "A", 1); graph.insertEdge("F", "C", 2); graph.insertEdge("A", "D", 5); graph.insertEdge("A", "B", 4); graph.insertEdge("E", "B", 2); graph.insertEdge("D", "F", 4); graph.insertEdge("D", "E", 3); graph.Select(); graph.Select1("A"); //[A,B,D,E,F,C] graph.Select2("A"); //[A,B,D,E,F,C] test88 aa = new test88(); aa.insertNode("A"); aa.insertNode("B"); aa.insertNode("C"); aa.insertNode("D"); aa.insertNode("E"); aa.insertNode("F"); aa.insertNode("G"); aa.insertNode("H"); aa.insertEdge("A", "D", 3); aa.insertEdge("A", "G", 3); aa.insertEdge("B", "A", 3); aa.insertEdge("B", "F", 3); aa.insertEdge("C", "H", 3); aa.insertEdge("D", "F", 3); aa.insertEdge("E", "B", 3); aa.insertEdge("F", "C", 3); aa.insertEdge("G", "E", 3); aa.Select(); aa.Select1("A"); //[A,D,F,C,H,G,E,B] aa.Select2("A"); //[A,D,G,F,E,C,B,H] } }
package com.lagou; import java.util.*; /** * */ public class test99 { //这里要考虑一个问题,如果广度操作最短路径那么需要某些自定义的变量吗,我稍微思考了一下: //实际上单纯的循环就能解决,比如我指定起点A,终点E,那么我们循环A的所有节点,然后对其节点对应的顶点也进行循环,自然也能找到终点E //所以我们唯一要做的,就是如何保存路径,我们也知道他的最短路径是ADE,可以怎么保存呢,因为我们第一次扩散,是BE,他的对应A节点可以保存,但是若到第二个扩散D开始时,虽然找到E,但是你总不能格外定义一个变量保存他的A吧 //就算你能,那么第三次扩散,第n次扩散,你怎么保存,所以只要解决保存的问题,那么该最短路径就解决了 //我思考良久,我决定加上一个变量,我对每个边(Edge)中加上头节点变量 public class Vertex { String name; Edge next; Vertex prev; //用来保存头节点,只要每个节点都进行保存,那么就能够得出路径,但是有个问题,该变量在添加边时,需要将指向的节点的该变量进行设置吗,答:不要,因为添加的节点中,因为一个节点是可以被多个节点指向的,所以不会让他进行初始化头 //我们这个变量只是为了给广度进行操作,因为标志的原因,所以其他节点是就不能再次的指向他了,所以只在对应的广度里进行操作 //注意:这里是给节点,而不是边,因为边只有对应的节点才有,到其他节点了,那么边就不同了(虽然是相同的对应节点名称,但是还是不同的类对象),再说,该边的next只是保存对应同一顶点的边的 boolean b = false; public Vertex(String name, Edge next) { this.name = name; this.next = next; } } public class Edge { String name; int weight; Edge next; public Edge(String name, int weight, Edge next) { this.name = name; this.weight = weight; this.next = next; } } Map<String, Vertex> map; public test99() { map = new HashMap<>(); } public void insertNode(String a) { if (map.get(a) == null) { Vertex aa = new Vertex(a, null); map.put(a, aa); } } public void insertEdge(String a, String b, int c) { Vertex vertex = map.get(a); if (vertex == null) { vertex = new Vertex(a, null); map.put(a, vertex); } if (vertex.next == null) { vertex.next = new Edge(b, c, null); } else { Edge temp = vertex.next; Edge temp2 = vertex.next; while (true) { //为了后续队列的操作,这里需要进行大小的判断,由于不用考虑头,所以代码相对简单 //防止添加相同的边 if (temp.name == b) { temp.weight = c; return; } if (temp.name.hashCode() > b.hashCode()) { //那么就说明新添加的比较小,所以对应的要放在前面 if (temp2 != vertex.next) { //进入这里,说明他已经经历了一个循环 Edge j = new Edge(b, c, null); temp2.next = j; j.next = temp; } else { //进入这里,说明就是第一次 Edge j = new Edge(b, c, null); vertex.next = j; j.next = temp2; } return; } if (temp.next == null) { temp.next = new Edge(b, c, null); return; } //其他情况就是大的,当然,这里可以不用判断,因为外面就是大的了,虽然也可以判断,之前的链表操作就是判断的,这里就不判断了,反正是一样的 temp2 = temp; temp = temp.next; } } } public void Select() { Set<Map.Entry<String, Vertex>> entries = map.entrySet(); Iterator<Map.Entry<String, Vertex>> iterator = entries.iterator(); while (iterator.hasNext()) { Map.Entry<String, Vertex> entry = iterator.next(); Vertex vertex = entry.getValue(); Edge edge = vertex.next; while (edge != null) { System.out.println(vertex.name + " 指向 " + edge.name + " 权值为:" + edge.weight); edge = edge.next; } } } //现在我们来编写广度优先搜索 public void Select2(String a) { Set<String> strings = map.keySet(); Iterator<String> iterator = strings.iterator(); while (iterator.hasNext()) { String next = iterator.next(); map.get(next).b = false; } String[] array = new String[map.size()]; int i = 0; //首先得到该节点 Vertex vertex = map.get(a); //定义一个队列 Queue<String> queue = new LinkedList<>(); queue.offer(vertex.name); //入队 vertex.b = true; //设置标记 //出队 String poll = queue.poll(); array[i] = poll; i++; while (true) { //操作入队 Edge edge = vertex.next; if (edge != null) { while (edge != null) { if (map.get(edge.name).b == false) { queue.offer(edge.name); map.get(edge.name).b = true; //标记 } edge = edge.next; } } //最后一个出队也到这里,说明操作完毕 if (queue.isEmpty()) { break; } //上面将相关的都入队后,然后外面操作出队,若没有入队的,那么自然是对应下一个出队 String poll1 = queue.poll(); array[i] = poll1; i++; vertex = map.get(poll1); //得到出队的顶点 edge = vertex.next; //操作出队顶点的入队 } //打印遍历的信息 System.out.print("["); for (int ii = 0; ii < array.length; ii++) { if (ii == array.length - 1) { System.out.print(array[ii]); } else { System.out.print(array[ii] + ","); } } System.out.println("]"); } //至此,广度优先遍历操作完毕 //这里的代码我进行了没有必要的删除,因为深度(深度优先搜索)是不需要的,现在我们任然是编写一个方法,代码与前面的广度的代码基本类似 //只是需要部分修改 public void Select3(String a, String b) { //b代表终点 Set<String> strings = map.keySet(); Iterator<String> iterator = strings.iterator(); while (iterator.hasNext()) { String next = iterator.next(); map.get(next).b = false; } String[] array = new String[map.size()]; int i = 0; //首先得到该节点 Vertex vertex = map.get(a); //定义一个队列 Queue<String> queue = new LinkedList<>(); queue.offer(vertex.name); //入队 vertex.b = true; //设置标记 //出队 String poll = queue.poll(); array[i] = poll; i++; //定义最终的节点 Vertex l = null; while (true) { //操作入队 Edge edge = vertex.next; if (edge != null) { int o = 0; while (edge != null) { if (map.get(edge.name).b == false) { queue.offer(edge.name); map.get(edge.name).b = true; //标记 } map.get(edge.name).prev = vertex; if (edge.name == b) { l = map.get(edge.name); o = 1; break; } edge = edge.next; } if (o == 1) { break; } } //最后一个出队也到这里,说明操作完毕 if (queue.isEmpty()) { break; } //上面将相关的都入队后,然后外面操作出队,若没有入队的,那么自然是对应下一个出队 String poll1 = queue.poll(); array[i] = poll1; i++; vertex = map.get(poll1); //得到出队的顶点 edge = vertex.next; //操作出队顶点的入队 } //打印路径 String[] aaa = new String[map.size()]; int oo = 0; for (int ii = 0; ii < aaa.length; ii++) { aaa[ii] = l.name; oo++; l = l.prev; if (l == null) { break; } } //进行提取路径 System.out.println(Arrays.toString(aaa)); String[] bbb = new String[oo]; for (int pp = 0; pp < bbb.length; pp++) { bbb[pp] = aaa[pp]; } String[] ccc = new String[bbb.length]; //因为他的路径是反的,所以再次的反过来 for (int pp = 0, ss = ccc.length - 1; pp < ccc.length; pp++, ss--) { ccc[pp] = bbb[ss]; } System.out.println("路径如下:" + Arrays.toString(ccc)); } public static void main(String[] args) { test99 graph = new test99(); graph.insertNode("A"); graph.insertNode("B"); graph.insertNode("C"); graph.insertNode("D"); graph.insertNode("E"); graph.insertNode("F"); //给c这个顶点,添加A这个顶点,对应的权值是1,后面依次类推即可 graph.insertEdge("C", "A", 1); graph.insertEdge("F", "C", 2); graph.insertEdge("A", "D", 5); graph.insertEdge("A", "B", 4); graph.insertEdge("E", "B", 2); graph.insertEdge("D", "F", 4); graph.insertEdge("D", "E", 3); graph.Select(); graph.Select2("A"); graph.Select3("A", "E"); //[A, D, E],这就是最短路径 } } //从这里我们可以发现,对应的图的确是由顶点和边组,因为边也能够得到具体的顶点,当然如果他也是当前类,那么也是一样的得到,因为name的存在可以使得map来得到无论他是当前Vertex类(前面说明操作权重的是否需要相同类的哪个地方进行了说明,即"当然,你可能会问,设置相同的类不就好了,实际上也行"),还是定义的Edge类 //所以顶点的设计,以及边的设计,和集合的设计,都是有深意的
package test; /** * */ public class test1 { //这里我们定义一个类,来代表商品的关系(对象的概念) public static class Goods { String name; //物品名称 double weight; //物品重量,实际上通常需要double来表示实际重量,因为在生活中都基本是有小数的 double price; //物品价格 String k = ""; //为了给不排序进行操作的变量 //这里就不给出具体一千克多少元的变量了,因为实际上生活中,是需要我们自己计算的 public Goods(String name, double weight, double price) { this.name = name; this.weight = weight; this.price = price; } } //既然类已经定义好了,那么我们在测试时,必然是需要操作三个他这个对象的,所以具体方法如下: //参数1:商品列表,我们通常需要将他进行排序,参数2:我们需要的最大重量,即背包最大重量 public void take(Goods[] goods, double a) { //我们先对该列表进行排序,那么有个问题,可以不排序吗,并且,不排序的时间复杂度会低吗,在代码中,我会给出两种操作,一个是排序的,一个是不排序的 //即在后面我会给出一个没有排序的方法,你可以进行对比,来观察时间复杂度 //进行排序 Goods[] sort = sort(goods); double j = 0; //我们来决定操作拿什么 for (int i = 0; i < sort.length; i++) { //我们先判断我们的容量是否可以全部拿取 if (a >= sort[i].weight) { a -= sort[i].weight; System.out.println(sort[i].name + "全部拿走,总共:" + sort[i].weight + "kg,总价值" + sort[i].price); j += sort[i].price; } else { //如果容量不能都进行拿取,那么只能是取一部分了 System.out.println(sort[i].name + "部分拿走,总共:" + a + "kg,总价值" + a * sort[i].price / sort[i].weight); j += a * sort[i].price / sort[i].weight; a -= a; break; } } System.out.println("总共得到" + j + "元"); System.out.println("背包剩余容量:" + a + "kg"); } //如果不操作排序,那么需要如下代码: public void take1(Goods[] sort, double a) { double j = 0; //我们来决定操作拿什么 for (int i = 0; i < sort.length; i++) { //找到最好的商品 int ii = 0; Goods g = sort[ii]; while (true) { if (ii + 1 >= sort.length) { break; } if (g.k.equals("不存在")) { g = sort[ii + 1]; ii++; continue; } if (g.price / g.weight < sort[ii + 1].price / sort[ii + 1].weight) { g = sort[ii + 1]; } ii++; } //我们先判断我们的容量是否可以全部拿取 if (a >= g.weight) { a -= g.weight; System.out.println(g.name + "全部拿走,总共:" + g.weight + "kg,总价值" + g.price); j += g.price; } else { //如果容量不能都进行拿取,那么只能是取一部分了 System.out.println(g.name + "部分拿走,总共:" + a + "kg,总价值" + a * g.price / g.weight); j += a * g.price / g.weight; a -= a; break; } g.k = "不存在"; } System.out.println("总共得到" + j + "元"); System.out.println("背包剩余容量:" + a + "kg"); } //我们可以发现,他里面嵌套了循环,那么他最少也是n^2,而不排序的基本是n^2+n(虽然有优化) //即,我们可以发现,不排序的时间复杂度竟然比较低,所以有时候我们并不会必须排序,我们通过观察可以发现,实际上是因为先排序的,他需要保存,而不排序的直接使用了,所以减少了保存的时间复杂度,也就是n //但是,不排序的真的好吗,答:在非常多的操作中,我们最好进行排序,虽然这里他看起来不排序的好,那么如果后面又有一个操作,比如我需要你输出从大到小的对应每1kg多少元,很明显,他要进行循环,是n^2,而先排序的,只需要一个循环即可,即O(n),所以这个时候,排序的就只需要n^2+2n,但是不排序的却需要2n^2,很明显,如果n是2,那么自然相等,但是一般n都是有很多的,所以通常来说排序的好,即我们通常会使用排序的 //所以实际上先排序的可以重复利用,只是他的一开始需要更多的时间复杂度而已 //那么结合上面的,我们知道,排序的可以重复利用,那么我们可以选择忽略排序的时间复杂度(只是这里,其他地方要的话,我们通常不会忽略) //那么该贪心算法的(该操作),只需要O(n)即可,n是商品数量 //定义排序方法 public Goods[] sort(Goods[] a) { //根据需求,排序是需要根据其重量对应的元来进行的,根据前面学习的排序,这里我们使用冒泡排序,因为最简单,虽然时间复杂度比较高 for (int i = 0; i < a.length - 1; i++) { boolean is = true; for (int j = 0; j < a.length - 1 - i; j++) { Goods tmp = null; if ((a[j].price / a[j].weight) < (a[j + 1].price / a[j + 1].weight)) { is = false; tmp = a[j]; a[j] = a[j + 1]; a[j + 1] = tmp; } } if (is) { break; } } return a; } public static void main(String[] args) { test1 bd = new test1(); Goods goods1 = new Goods("A", 10, 60); Goods goods2 = new Goods("B", 20, 100); Goods goods3 = new Goods("C", 30, 120); Goods[] goodslist = {goods1, goods2, goods3}; bd.take(goodslist, 50); bd.take1(goodslist, 50); } }
package test; /** * */ public class test2 { //定义变大写的方法 public static char toUpCaseUnit(char c) { //因为在ASCII中,a代表97,z代表122,所以,如果对应的字母越界了,那么直接的返回空字符(没有''),字符必须要有个值 int n = c; if (n < 97 || n > 122) { return ' '; } //如果在该范围里面那么减去32即可 return (char) (n - 32); } //定义了对应的转换,那么我们来操作他 //参数1:代表对应的字符串的字符,因为这里是递归,所以需要有对象操作(数组),来使得递归的方法也能继续操作 //参数2:代表从哪个下标开始进行变大,下标自然包括0 public static char[] toUpCase(char[] chs, int i) { if (i >= chs.length) return chs; //如果超过了,说明没有什么需要进行变大的,自然返回自身 chs[i] = toUpCaseUnit(chs[i]); //将该字符进行变大 return toUpCase(chs, i + 1); //重新执行当前方法,该就是式子操作,而并非一定是关系式 //很明显,他最终都会返回return chs,即当前数组 //实际上我们也可以使用循环来操作 } //实际上我们也可以使用循环来操作 //具体方法如下 public static char[] toUpCase1(char[] chs, int i) { for (int a = i; a < chs.length; a++) { chs[a] = toUpCaseUnit(chs[a]); } //都进行变大后,直接的退出,不用考虑判断了 return chs; } public static void main(String[] args) { String ss = "abcde"; System.out.println(test2.toUpCase(ss.toCharArray(), 0)); System.out.println(test2.toUpCase1(ss.toCharArray(), 0)); } }
package test; /** * */ public class test3 { public static int commpow(int x, int n) { //假设x是2,n是10 int s = 1; while (n >= 1) { //因为n是10,所以这里会执行10次 s *= x; //执行10次,那么会乘以10个2,也就是2^10了 n--; } return s; } public static void main(String[] args) { System.out.println(commpow(2, 10)); } }
package test; /** * */ public class test4 { public static int dividpow(int x, int n) { //假设x是2,n是10 //递归结束 任何数的1次方都是它本身 if (n == 1) { return x; } //每次分拆成幂的一半 int half = dividpow(x, n / 2); //很明显,他里面会操作其一般的次幂 //偶数 if (n % 2 == 0) { return half * half; } else { return half * half * x; } } //看代码,你可能很难看出,现在,我给出具体的流程 //当2,10进入后 /* 2,10进入的half等待获取值,2,5进入后等待获取值,2,2进入后等待获取值,2,1进入后,返回一个2 2,2,得到该2,返回2*2 2,5,得到2*2,返回(2*2)*(2*2)*2 2,10,得到(2*2)*(2*2)*2,返回(2*2)*(2*2)*2*(2*2)*(2*2)*2,即2^10,至此方法真正的结束 所以上面的: if (n % 2 == 0) { return half * half; } else { return half * half * x; } 就是操作其次幂,这里你最好多思考一下 实际上是因为底层最里面的原因,导致2这个值一直返回,而其上面的都操作返回的值,而因为奇数的存在,所以导致奇数需要多出来一个2 至此,底层的原因说明完毕,具体如何体会,看你自己吧 */ public static void main(String[] args) { System.out.println(dividpow(2, 10)); } } //上面是从低到高的进行说明该算法,现在你再看看之前的图片,可以发现,实际上他的每一次操作,都会结合返回值来进行重合,即从高到低的进行说明 //所以就会出现前面图片的情况,因为加起来的half就是前面的图片最后的组合
package test; /** * */ public class test5 { //皇后,在上下直线以及其四个方向的对角线都能进行攻击(具体百度查看即可),所以这里要考虑这些情况 //要解决n皇后的问题,我们知道,要添加皇后时,他的主要问题是在添加这个皇后之后,其他的皇后不能在他的对角线上(斜线)以及直线上 //那么如何操作对角线和直线呢,且加上皇后之后,那么后面每次添加皇后,都必须要前面的皇后进行判断,这非常困难 //当然,虽然很困难,但是还是可以操作的,话不多说,先看代码,在代码中进行说明 //首先我们先定义皇后数量,我们将棋盘从0开始,到7就行(行和列都是这样),那么棋盘最大数量的皇后必然只能是8个,因为对角线和直线的存在,所以皇后只能是8个(主要是因为直线) static int QUEENS = 8; //那么如何定义棋盘的,由于皇后是8个,那么整个棋盘中,其中一行也只能是一个(直线只能存在一个皇后),即很明显,我们可以定义一个数组即可 int[] result = new int[QUEENS]; //我们将下标表示行,里面的值表示存储在哪一列即可(这里是覆盖的必然条件,在后面会有解释为什么要这样) //定义多少可能 int aa = 0; //定义执行次数 int jj = 0; //现在,我们以及定义好棋盘了,那么现在我们来编写主要代码: //首先是存放皇后的位置,参数a代表要添加的行 public void insert(int a) { //这里我会使用递归来操作,当然,如何操作看后面即可 //首先操作结束条件 if (a >= QUEENS) { //只要==即可,但为了以防万一(一般是高并发才会出现"万一"的情况,即出现问题,那么在高并发下,我们最好设置>=,这里可以只要==即可,反正基本不会出现什么问题),这里设置>= printQueens(); //添加完毕,才会操作打印,这里的理解,也要看后面 aa++; return; } //现在,我们来进行放入皇后 for (int i = 0; i < QUEENS; i++) { //这个i代表列,那么自然这个列不能超过8 //System.out.println("执行第" + ++jj + "次循环"); //这里的+要进行分开,否则会报错,比如:因为不知道是+(连接)还是++(操作值的加1) //++jj; if (isOK(a, i)) { //判断是否可以放置,即在该行和该列是否可以放置 //如果可以放置,那么我们进行放置 result[a] = i;//该行的该列(值) //开始下一行 insert(a + 1); //这里我会进行大致说明: //加上了这个,代表我们已经操作完毕,为什么,这里你仔细思考一下回溯 //如果我们这里添加成功一次,那么当insert(a+1);返回后 //其他的循环,是不是必然直接的跳过,而不会操作isOK(a,i)为true,答:不是,如果是上面的,必然会导致在这种情况下,出现问题: /* 1 0 0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 1 0 0 0 0 1 0 0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 在这一行,我们可以发现,他必然是添加不了的,也就是说,他会往上走,当然,我们判断isOK中,判断是是竖直的直线,所以这里会延续其列 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 会改变其该列,即变成了 1 0 0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 1 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 1 因为只有一个列,所以数组覆盖后,那么就变成了这里 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 当然有很多种这样的情况,但是无论你怎么存放,对角线都充足的,所以通过上面的操作,必然会导致一个结果出来,这里的最终结果是: 0 0 0 0 0 0 0 1 0 0 0 1 0 0 0 0 1 0 0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 1 0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 你可能会有疑惑,这样也行啊: 1 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 1 0 0 1 0 0 0 0 0 0 0 0 0 1 0 0 0 0 但是打印信息为什么会有多个 这里就要格外考虑了: 我们查看上面的这个代码: if (a == QUEENS) { printQueens(); //添加完毕,才会操作打印,这里的理解,也要看后面 return; } 我们可以发现,当全部满足时,可以进行返回,但是你要知道,他的满足中,前面的路径是可以改变的,因为不会判断水平和左下角和右下角,比如其中第一行的位置是能够再次的覆盖移动,通过这样的说明你应该可以发现,他就是打印出所有的情况 以第一行为主(因为是查看左上角和右上角的,而不会查看左下角和右下角的),其他行满足前面的行时,慢慢的往下制造打印的可能,然后可以改变前面的行,然后如此反复,自然就是打印所有情况,即这的确利用的回溯的本意"我们枚举所有的解,找到满足期望的解,为了有规律地枚举所有可能的解,避免遗漏和重复,我们把问题求解的过程分为多个阶段,每个阶段,我们都会面对一个岔路口,我们先随意选一条路走,当发现这条路走不通的时候(不符合期望的解),就回退到上一个岔路口,另选一种走法继续走" 即可以这样的认为,如果是: "111"数字 让你列举111到999的所有的可能,那么自然是112,113,当119后,然前一个进行加1,这就是回溯的一个意思,上面的n皇后也是利用这样,当后面的完全满足时,或者到尽头了,那么前面的可以进行移动了 */ //这是因为数组的特性(对同一个下标进行更新,那么就相当于覆盖操作了),所以可以这样的覆盖,所以在isOK中,我才说,数组隐含的表示了水平,也就是说,实际上我们也判断了其他的可能,即回溯了,也检验了"枚举所有的解",避免遗漏和重复 //即检验了上一个的不是吗,所以在回溯的介绍中,我们也说明了"当发现这条路走不通的时候(不符合期望的解),就回退到上一个岔路口,另选一种走法继续走" //所以与递归不同的是,他的返回是能够在上一个继续的递归,而不是返回后,继续在上一个去返回,这就是回溯的基本思想 } //如果不可以放置,那么在该行的其他列进行放置,因为其直线和对角线的原因,实际上必然可以放置8个(因为对角线是大于8的,即有15个,才能全部覆盖,但只能添加8个,因为直线的原因,即直线决定上下和位置,对角线只决定位置) //至此,只有对应的所有循环都结束,那么打印才会结束,即代码执行完毕 } } public void printQueens() { for (int i = 0; i < QUEENS; i++) { //定义行 System.out.print("|"); for (int j = 0; j < QUEENS; j++) { //定义列 if (result[i] == j) { System.out.print("Q|"); } else { System.out.print("*|"); } } System.out.println(); //换行 } System.out.println("-----------------------"); } //这里是n皇后的主要问题,用来确定是否在对角线或者直线上有皇后 public boolean isOK(int a, int b) { //参数a代表该行,b代表该列,我们主要的就是判断该行该列的对角线或者直线上是否有皇后 int left = b - 1; //既然是对角线,那么首先线得到左边的对角线的第一个位置列 int right = b + 1; //得到右边对角线的第一个位置列 //那么我们是看上面的对角线,还是看下面的对角线呢(对角线有四个方向),答:我们只需要看上面的对角线,因为我们是往下添加的(即我们只看左上和右上),因为下面必然是没有皇后的(我们才刚刚添加,即尾部行添加,那么自然下面的行没有皇后) //那么我们就逐级的判断上一行即可 int o =jj; for (int i = a - 1; i >= 0; i--) { //先从当前的上一行开始,然后依次的判断上面其他的行 ++jj; //首先看他有没有在当前直线上(竖直的直线,因为水平的直线,已经通过数组隐含的表示了,在上面回溯中会说明,即上面的"这里你仔细思考一下回溯"那个地方)是否有皇后,因为这也是需要判断的(即这里补充) if (result[i] == b) { //如果上一行的值(也就是列,这个下标是行),与当前的列(值)相同,代表是一个直线 return false; //返回false,代表有皇后 } //查看左上角是否有皇后 if (left >= 0) { //自然不能超过棋盘,否则是默认不存在皇后的,自然不会对该情况操作出返回false,相当于在最左边添加,那么自然不用考虑左边的对角线 if (result[i] == left) { //若对应上一行的值等于该列(下标是代表行的,值是代表列的),那么说明对角线有皇后,即返回false return false; } } if (right < QUEENS) { //与上面同理,这里没有=,当然加上也没有事情,因为列的值是不可能是QUEENS的,如果操作等于,那么在最右边添加,那么等于=就会出现对应的操作(与上面的在最左边添加相反) if (result[i] == right) { return false; } } //如果没有返回,说明该两个位置列判断完成,那么我们再次的移动列 left--; right++; } if(o==jj){ jj++; //操作这个最终的jj是40290,因为外循环本身也算,和内循环结合也算一起(因为也是一起的) } //当上面的对角线和直线都判断没有皇后,那么说明该行和该列可以添加,自然返回true return true; } public static void main(String[] args) { test5 queens = new test5(); queens.insert(0); //从0行开始添加 System.out.println("有" + queens.aa + "个可能"); //92个可能 System.out.println("执行了" + queens.jj + "次循环"); //40282次循环(实际上总共是40290,因为会第一行是直接跳过的,但是有8次操作,如果操作了上面的o==j就是40290),而8的阶乘是40320 //这个次数有外面的循环和内循环总体来的 //但也可以大致的认为是8的阶乘 //当然,你可以设置对应的QUEENS=4,会发现,只有2个可能,但有78次循环 //而4*3*2*1=24,比较多了,而对于3来说是没有可能的,即0个可能 //你可能会有疑惑,按道理来说无论是设置8还是4,应该是阶乘的次数,但是为什么不是呢 //现在我们来进行调试,很困难哦 /* //现在就是第一次: 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0 0 1 0 到这里,需要3次 0 0 0 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 因为2+1+1+1=5次 0 0 0 0 1 0 0 0 0 0 0 1 再来1次,那么这里的4次操作完毕,现在总共是10次了(包括该1次) 0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 1 0 1 0 0 总共需要2+2=4次 0 0 0 0 1 0 0 0 0 0 0 1 0 1 0 0 0 0 0 0 需要1+1+1+2=5次,现在是19次了 1 0 0 0 0 0 0 1 0 1 0 0 这里往后移动需要1+1=2次,即21次 0 0 0 0 经过21次后 1 0 0 0 0 0 0 0 由于他的执行是直接的退出,那么可以发现,上面的1需要移动 0 0 0 0 0 0 0 0 至此,第一行的1代表21次,很明显,与你认为的4*4*4=64少很多次数 现在我们来分析上面的情况,我们先看第二行,他总共执行次数是4,没有问题 第三行就有问题了,即5+4+2=11次,从这里你应该可以看出,并不是每一次的上一行都会使得下一行进行操作,比如第二行只有两次会影响下一行,所以4*4*4*4是不正确的,而(3*1)*(2*1)*(1*1)*4=3*2*1*4(第一行操作4次),即认为他们只判断1次,这也是不正确的(因为有些执行2次,有些执行1次就退出),很明显,次数在他们中间,因为他们始终进行变化,所以需要需要考虑边界的问题,所以需要的次数在4*3*2*1<x<4*4*4*4(x是需要的次数) 至此,他并没有什么规律,所以时间复杂度模糊不定(因为对角线和直线的存在是变化的,因为可以放在最左边和最右边,使得只有两个一次,而不是三个,或者导致只有少的次数影响下一行,所以模糊不定),但是我们认为他是按照理想情况下的最小进行操作,所以我们也认为这里的时间复杂度是n!,即4*3*2*1 */ } }
package test; /** * */ public class test6 { //用于存储每次的计算结果 static long[] sub = new long[100]; public static long fib(int n) { if (n <= 1) return n; //没有计算过则计算 if (sub[n] == 0) { sub[n] = fib(n - 1) + fib(n - 2); //之前的递归,基本是直接的返回该值,现在我们直接的保存 } //计算过直接返回 return sub[n]; } //之前的代码 public static long fa(int n) { if (n <= 1) return n; return fa(n - 1) + fa(n - 2); } public static void main(String[] args) { System.out.println(fib(50)); //50的话就是96次操作(调用自身) //可以发现,他的执行出现的结果比之前的快了很多(也就是下面的打印中使用的方法,就是之前的) System.out.println(fa(50)); //50的话就是96次,这个说明在下面的解释中 } }
/*
如果i<=1,那么dp[0]=0,dp[1]=1,dp代表上图中下面的类似数组
如果i>1,那么就是dp[i]=dp[i-1]+dp[i-2],这就是dp方程
最优子结构:如果我们查询的是第9个下标,那么就是fib[9]=fib[8]+fib[7],即最大的情况(也是函数的功能,因为的确要这样加)
边界:a[0]=0; a[1]=1;,可以看到的确类似于结束条件
dp方程:fib[n]=fib[n-1]+fib[n-2]
我们也可以发现,实际上算法的思想,基本都与递归有关,当然,虽然是不同的思路,可以具体的意义不同,侧重点不同
贪心侧重于当前(直接的解决即可,看最好情况)
分治侧重于下一个(子问题,相当于递归)
回溯侧重于上一个(使用循环来操作上一个)
而动态规划侧重于所有情况,通常我们也用来保存最优解(主要侧重)
他们基本都利用了递归
所以说,当某些问题需要你使用上面的情况时,自然是需要你进行侧重点进行出发
*/
package test; /** * */ public class test7 { public static int fib(int n) { int a[] = new int[n + 1]; a[0] = 0; a[1] = 1; int i; // a[n]= a[n-1]+a[n-2] for (i = 2; i <= n; i++) { a[i] = a[i - 1] + a[i - 2]; //直接得到上一个 } // i已经加1了,所以要减掉1 return a[i - 1]; } //之前的循环是: public static int fa(int i) { if (i <= 1) return i; int a = 0; int b = 1; for (int j = 2; j <= i; j++) { b = b + a; a = b - a; //进行移动 } return b; } //很明显,上面的保存是按照数组来保存的,因为循环下标(参数)正好可以对应数组下标 //这里稍微思考一下就是: //0 1 1 2 3,要得到5 //前面的一种是,将数组对应下标的2的值和3的值相加到上一个下标(因为会有上一个,所以长度要比传递进来的参数要加1 //而后面的一种就是2加上3,然后移动b到5,a到3 public static void main(String[] args) { System.out.println(fib(9)); //50的话就是49 System.out.println(fa(9)); //50的话就是49 } }
/* 1:定义快慢两个指针: slow=head; fast=head.next; 2:遍历链表(这里不考虑哈希的操作): 快指针步长为2:fast=fast.next.next; 慢指针步长为1:slow=slow.next; 3:当且仅当快慢指针重合(slow==fast),有环,返回true 4:快指针为null,或其next指向null,没有环,返回false,操作结束 那么要不要考虑环的大小,答不用考虑,如果存在环,说明他们的移动必然会导致相同 那么为什么设置快的为2,可以设置3,4,5吗,最好不要,因为我们是需要注意fast.next.next中next是否为null的情况 那么如果解决这个问题,那么可以随便移动快的位置吗,答:可以 你可能会认为出现这样的 假设是:1 2 3 4 5,5指向3,我们设置快指针是4,首先假设快的先操作,那么直接到5,慢到2 以此类推:3 3,4 4,重合了,这一个例子你可能会认为不够 那么再次的假设是:1 2 3 4 5 6,6指向3,我们设置快指针是5,那么为什么上面是4,这里是5呢,这是为了保证在里面可以无限循环,比如5后面到6,那么他是慢慢加1的,而慢指针也是慢慢加1,所以是不会重合,即无限循环了,而其他的情况,必然会导致重合,只是时间问题而已,因为你始终接近慢的1,但是你可以发现,就算是设置5,那么一开始就是6,2,然后3,3,也重合的,所以我们才会说,可以随便移动快的指针,但是由于需要注意fast.next.next中next是否为null的情况,我们才只会操作设置为2的快指针(实际上因为对应的环对应设置,所以导致第二次移动时,必然会导致相等,所以这也是虽然可以移动的原因) //这里我们就以快指向移动2,慢指针移动1开始操作 */
package test; /** * */ public class test8 { //定义节点类 public static class Node { int key; String name; Node next; public Node(int key, String name) { this.key = key; this.name = name; } } //我们直接定义方法: public static boolean isRing(Node head) { if (head == null) { System.out.println("没有头节点"); //进行提示,使得与环的问题相离 return false; //如果对应没有节点,那么自然直接的返回false, } //定义初始的快慢指针 Node slow = head; //慢指针 Node fast = head.next; //快指针 while (fast != null && fast.next != null) { //之所以将fast != null放在前面,是防止fast本身是null,导致fast.next报空指针异常,这是因为&&只要出现false,那么后续条件不会执行操作了 //查看其当前或者他的下一个是否是null,如果是null,说明他是null或者下一个是null,那么就没有环了,因为已经到顶了 //有环,顺便可以判断初始的指针 if (slow == fast) { return true; } fast = fast.next.next;// 快指针步长为2 slow = slow.next;//慢指针步长为1 } return false; } public static void main(String[] args) { Node n1 = new Node(1, "张飞"); Node n2 = new Node(2, "关羽"); Node n3 = new Node(3, "赵云"); Node n4 = new Node(4, "黄忠"); Node n5 = new Node(5, "马超"); n1.next = n2; n2.next = n3; n3.next = n4; n4.next = n5; n5.next = n1; System.out.println(isRing(n1)); } }
/* 前面只是给出最好的价值出现的原因(逻辑思考的),那么在程序里面或者说有没有具体流程来使得得到最好的价值,即我们应该如何得到最好的价值呢,这是完成0-1背包的核心问题,只要解决了这一个问题,那么说明o-1背包问题解决,现在我们进行分析,由于我们是按照顺序来的,也就是先看前面的组合,然后看后面的组合,那么有以下的分析: 在保证前面的组合进行放入的情况下,再次考虑后续组合,这里最好理解,这是核心 也就是说,先判断当前所操作的行,然后来进行添加,如果我们的循环的重量,比如前面的j是1,i也是1,由于j是小于当前物品重量的,那么取前面一个值,即上一行(前面的所有物品)的值(也就是0,即价值),这是上面二维数组,为什么从1开始的主要原因,否则我们需要进行判断,使得如果是-1那么设置为0价值(后面会说明,即uu=0那里),那么需要格外的代码了,为什么取前面一个值的原因是:既然你的重量不满足,那么自然就是前面的是最好的值(因为保存的都是最好的) 如果循环的j是大于等于当前物品重量的,比如j是2,那么判断如下:"当前上一行的该重量价值"与"当前物品价值+剩余重量在上一行中对应的价值",谁大取哪个,这里就是核心的列子 既然我们知道了核心列子,我们主要分析这个:"当前上一行的该重量价值"与"当前物品价值+剩余重量在上一行中对应的价值",为什么要这样的设置,看如下解释: 先说明他的公式(式子或者方程)是否正确: 假设i是2,j是3,首先当前上一行的价值是6,但正是因为我们可以进行放入该物品 所以我们需要考虑总体(就如前面的由9变成11的一样):当前物品(行)的价值是3,我们还剩余1个重量(因为3-2=1,注意:这个2是当前物品的重量),那么我们将这个剩余的重量进行查找,也就是从上一行找,因为上一行自然是其最好的价值,即我们找到上一行的第j是1的哪个值进行相加,很明显,他是0,所以当前上一行的价值是6,而当前物品价值加上上一行中剩余重量的最好值是3+0=3,那么取6,这里你可以发现,他的逻辑问题就是,如果满足当前的,那么剩余的重量取上一行的对应重量值,你可能还是有疑惑,为什么这样就能得出最好的结果呢(后面会进行说明的),那好,现在我们可以继续分析之前的第9个重量,即中间价值从上到下中的6 9 11 13 15中的11这个地方,使用具体流程来进行操作11 即主要参考其i是3,j是9的地方,我们可以发现,当前价值是5,9-6=3,还剩余3这个重量,那么我们来看看上一行中,3重量的最好价值,那么很明显就是6,所以他的结果就是11,因为当前上一行是9,11大于9,所以取11(当然,如果他们是相同的,那么自然取前者,在后面我们会使用Math.max,他的底层就是(a >= b) ? a : b;,即取前者) 至此,你可以发现,按照我们的逻辑是:如果有剩余重量,就比较当前上一行该重量的价值,与当前物品价值+上一行中剩余重量的对应的价值谁大,我们取大的,通过上面,可以发现,他的确能够解决最大的问题,可是你的疑惑可能还有,你可能认为上面不就是将公式进行运用吗,但没有说明为什么这样设置的原因,实际上上面的说明只是开始,即是让你知道他是可以作用的,即上面是前戏,现在才是重头戏: 我们知道按照前面的思考逻辑,我们在那里说明是通过重新判断来进行取得最大的值,但那里只是逻辑思考,没有具体流程而言,那么我们换一种思路,前面的说明是从上到下,那么我们现在从下到上: 假设他先取得当前所指向的物品,以i是1,j是2为例,那么j直到10都是6价值,当i是2时,j从2到10,在j是4时变成了9,我们先拿取当前的物品,然后拿取上一个的物品(从小到上),那么的确是9,注意:前面很明显的说明了,对应的j重量时,必然是最好的物品,因为都是从最好的情况来进行分析的,那么当i是3时,j从2到10,在j是8时变成了11,我们先拿取当前的物品,然后拿取上一个物品,当前的物品是(6,5)组合,接下来就是关键,因为我们是先拿当前物品,那么剩余的重量自然在上一行中就是其对应的最好物品,所以也就是3这个重量在上一行中是最好的物品,那么就是6的价值,即5+6=11,所以也就是说,满足自己,然后取剩余重量的最好情况 但是还有另外一种可能:如果当前物品价值加上剩余的价值,可能少于上一个,那么我们就要取上一个对应重量的价值 这里也有个问题,如果本身的重量大于其j重量,那么直接的取上一行重量对应的价值即可,否则看看上一行重量对应的价值与当前物品价值+剩余重量在上一行中对应的价值进行比较了,那么这里给出一个特殊情况,假设组合是: (2,99) (2,88) (4,5) 当先到(4,5),你可能会认为由于先加上(4,5)可能结果不对了,那这是错误的,因为还有上一行重量对应的价值我们会比较了,所以说,实际上当前物品价值+剩余重量在上一行中对应的价值就是为了考虑当前物品加上与之前的区别,之前的是没有加上了,而这里是加上的,所以"当前上一行的该重量价值"与"当前物品价值+剩余重量在上一行中对应的价值"用你能够理解的话语就是:"不考虑当前的物品"与"考虑当前的物品"的价值比较,由于保存的都是最好的,所有他们的重量操作是没有错误的,至此,说明完毕 这里我们假设定义一个数组,也就是上面图片中的数组,很明显,我们需要二维数组,因为0的存在,所以我们可以定义如下: int[][]dp=new int[values.length+1][max+1];,其中values.length是5,也就是左边的价值,max也就是最大重量 那么物品重量呢,我们使用int[] weights来表示对应价值的对应重量,因为我们会设置int[] weights与int[] values是对应的,所以通过相同下标可以找到对应的价值和重量 那么我们应该循环上面的图片二维数组的次数,也就是: for(int i=1;i<=values.length;i++){ for(int j=1;j<=max;j++){ 从1开始,因为0不用考虑,但是他还需要用处,后面你就知道了(因为从1开始,所以对应的weights下标需要减1): 现在我们选择一个物品,首先判断他是否大于总重量(背包) 如果w[i-1] >j,即若当前物品重量大于背包重量,那么直接取上一行该重量的值,也就是dp(i,j)=dp(i-1,j),因为0的存在,所以可以发现,0在这里是非常好的,即可以用来作为初始数据 而w[i-1]<=w,即若当前物品重量小于背包重量,那么我们就会考虑选择他,这时因为=的存在,所以会用到左边的0,那么0在这里也是非常好的 至此,上面大致说明完毕,现在,我们来进行编写: */
package test; /** * */ public class test9 { //参数1:物品价值,参数2:物品重量,参数3:背包重量,用来设置dp数组的j的最大值 public static int maxValue(int[] values, int[] weights, int max) { if (values == null || values.length == 0) return 0; if (weights == null || weights.length == 0) return 0; if (values.length != weights.length || max <= 0) return 0; //需要进行对应,否则直接返回,因为总不能一个物品对应两个重量或者两个价值吧,所以这里需要是长度相同的才可往下操作 //如果对应的数组是空的,或者背包是没有重量的,那么返回0,即什么都没有拿取 //dp数组 int[][] dp = new int[values.length + 1][max + 1]; //设置数组,我们以max为基准 //从1开始 for (int i = 1; i <= values.length; i++) { for (int j = 1; j <= max; j++) { //选择的物品超过最大承重 if (j < weights[i - 1]) { //最大价值=上一轮的最大价值(不选择当前物品,因为重量不够) dp[i][j] = dp[i - 1][j]; } //否则可选择该物品 else { //上一轮的最大价值(不选择该物品)与选择该物品进行比较,我们取两者的最大值 dp[i][j] = Math.max((dp[i - 1][j]), values[i - 1] + dp[i - 1][j - weights[i - 1]]); } } } return dp[values.length][max]; //返回最后一行,然后是max的重量即可 } public static void main(String[] args) { int[] values = {6, 3, 5, 4, 6}; int[] weights = {2, 2, 6, 5, 4}; int max = 10; System.out.println(maxValue(values, weights, max)); } } //如果你的循环非要从0开始,并且数组去除两边的0,那么可以操作如下代码: package test; /** * */ public class test9 { public static int maxValue(int[] values, int[] weights, int max) { if (values == null || values.length == 0) return 0; if (weights == null || weights.length == 0) return 0; if (values.length != weights.length || max <= 0) return 0; int[][] dp = new int[values.length][max]; //从0开始,去除了两边的0,即不用加1了 for (int i = 0; i < values.length; i++) { for (int j = 0; j < max; j++) { if (j + 1 < weights[i]) { //需要从1开始,使得与前面去掉的一样,因为前面也是没有操作0的位置 int l = 0; if (i != 0) { l = dp[i - 1][j]; } dp[i][j] = l; } else { int ii = 0; int oo = 0; if (i != 0) { ii = dp[i - 1][j]; int uu = j - weights[i]; //因为j+1,所以需要考虑下标越界,但由于去掉了对应的0,所以-1和0是相同的结果(因为-1和0就是原来的0,1的结果,都是0价值结果的意思),即uu设置为0,使得是相同的结果(即都是0价值的结果) if (uu < 0) { uu = 0; } oo = dp[i - 1][uu]; } dp[i][j] = Math.max(ii, values[i] + oo); } } } return dp[values.length - 1][max - 1]; } public static void main(String[] args) { int[] values = {6, 3, 5, 4, 6}; int[] weights = {2, 2, 6, 5, 4}; int max = 10; System.out.println(maxValue(values, weights, max)); } }
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。