极客时间-数据结构与算法之美

时间复杂度表达的是代码执行时间随数据规模增长的变化趋势。

时间复杂度分析:

  1. 只关注循环执行次数最多的一段代码
  2. 总的时间复杂度等于量级最大的那段代码的时间复杂
  3. 乘法法则:

O(1)表示代码执行时间不会随着n的变化而变化,无论n多大;只要代码中不存在循环语句和递归语句;即为O(1);

O(logn)分析:

         i=1;

         while(i

         i=i*2;

}

算代码的执行次数,即2的x次方为n的时候停止计算,那么x=log2N,故复杂度为O(log2N);不用管底数是什么,所有的复杂度都是O(logn);为什么呢?因为对数都是可以互相转换的,

log3N=log32*log2N,所以O(log3N)=O(C*log2N),其中C为常数,可以忽略系数;按照乘法法则可以计算出O(nlogn);

O(m+n):有两个输入的数据时,加法法则不再适用,但是乘法依然可以;

空间复杂度表示算法的存储空间与数据规模之间的增长关系;

常见的空间复杂度是O(1) O(n) O(n2)

课程5:

question:为什么数组下标从0开始计算?

answer下标的定义是相对于首地址的偏移量offset,否则在计算的过程中CPU会多一次减法的运算;

寻址方式:a[k]_address = base_address + k * type_size

Topic:GCC中的编译器堆栈保护技术

栈区的增长方向是从高地址到低地址-栈顶地址<栈底地址;

课程6 and 7: 链表

question循环链表解决约瑟夫问题?

answerhttps://blog.csdn.net/wenhai_zh/article/details/9620847(c语言)

question数组与链表的选择?

answer以空间换时间或者以时间换空间。             

极客时间-数据结构与算法之美_第1张图片

数组为连续存储空间,可以借助CPU缓存机制预读数据,访问效率高;

(解释:CPU在从内存读取数据的时候,会先把读取到的数据加载到CPU的缓存中。而CPU每次从内存读取数据并不是只读取那个特定要访问的地址,而是读取一个数据块,并保存到CPU缓存中,然后下次访问内存数据的时候就会先从CPU缓存开始查找,如果找到就不需要再从内存中取。这样就实现了比内存访问速度更快的机制,也就是CPU缓存存在的意义:为了弥补内存访问速度过慢与CPU执行速度快之间的差异而引入。)

链表在内存中不是连续存储,对链表进行频繁额插入删除操作,会导致频繁的内存申请和释放,容易产出内存碎片,java中容易GC。

Java中ArrayList虽然支持动态扩容,但是数据需要重新拷贝消耗时间;

question:反转单链表问题?

answer:             

极客时间-数据结构与算法之美_第2张图片

https://blog.csdn.net/fx677588/article/details/72357389

  1. 非递归方式:从头逐个倒序
  2. 递归方式:从末尾开始逐个倒序

question:回文链表问题?

answer1.利用快慢指针找到中点

	//判断是否为回文链表
	public static boolean isPalindrome(Node head) {
		Node slow = head;
		Node fast = head;
		while(fast != null && fast.next != null) {
			slow = slow.next;
			fast = fast.next.next;
		}
		fast = head;
		slow = reverseList(slow);
		
		while(slow != null) {
			if(fast.data != slow.data) {
				return false;
			}
			slow = slow.next;
			fast = fast.next;
		}
		return true;
	}

question:快慢指针问题

answer

  1. 是否存在环和寻找环的入口

https://www.cnblogs.com/songdechiu/p/6686520.html

https://blog.csdn.net/l294265421/article/details/50478818

http://www.cnblogs.com/xudong-bupt/p/3667729.html

思想:先找到相遇点;

question:写链表代码的技巧

answer:

             1.理解指针和引用,实际的意义就是存储所指对象的内存地址;

                 p->next=q  p节点中的next指针存储了q节点的内存地址;

             2.警惕指针丢失和内存泄露

                 插入节点的时候需要注意操作的顺序,警惕next的指向地址发生变化;

             3.利用哨兵简化实现难度(引入哨兵节点)

                 当插入头结点和删除尾节点的时候代码需要注意;哨兵节点不存储数据,head时钟指向哨兵节点

              4.重点留意边界位置         

课程8:栈

栈的应用-函数调用栈

操作系统给每个线程分配了一块独立的内存空间,这块内存被组织成栈结构,用来存储函数调用时的临时变量。没进入一个函数,就会将临时变量作为一个栈帧入栈,当被调用函数执行完成,返回后,这个函数对应的栈帧出栈。

栈的应用-表达式求值的应用

栈的应用-括号匹配中的应用

课程9:队列

question:向固定大小线程池中请求一个线程时,如果线程没有空闲资源,如何处理请求,排队还是拒绝?

answer:策略一:非阻塞处理方式,直接拒绝任务请求;

               策略二:请求排队,先进者先服务

question:对比基于链表和基于数组的实现方式

answer:基于链表支持无限排队的无界队列,但是可能导致过多的请求排队等待,请求处理时间长;基于数组的队列代销有限,当超过数组大小时请求会被拒绝。

  队列:操作受限的线性表数据结构

课程10:递归Recursion

去的过程叫做递,回来的过程叫做归。

满足递归的三个条件:

  1. 一个问题可以分解成子问题;
  2. 子问题的求解思路完全一致;
  3. 存在递归终止条件;

attention:递归要避免堆栈溢出;

通过限制递归调用最大深度的方式(在最大深度较小的情况下可以使用);

attention:递归代码要避免重复计算;

可以通过数据结构表(例如散列表)来保存求解过的值

课程12:归并排序与快速排序

归并排序采用分治合成的思想,复杂度O(nlogn)但不是原地算法,需要额外的空间,所以适用于小数据量;

快排更适合大数据量;

// 递归调用函数
  private static void mergeSortInternally(int[] a, int p, int r) {
    // 递归终止条件
    if (p >= r) return;

    // 取p到r之间的中间位置q,防止(p+r)的和超过int类型最大值
    int q = p + (r - p)/2;
    // 分治递归
    mergeSortInternally(a, p, q);
    mergeSortInternally(a, q+1, r);

    // 将A[p...q]和A[q+1...r]合并为A[p...r]
    merge(a, p, q, r);
  }
private static void merge(int[] a, int p, int q, int r) {
    int i = p;
    int j = q+1;
    int k = 0; // 初始化变量i, j, k
    int[] tmp = new int[r-p+1]; // 申请一个大小跟a[p...r]一样的临时数组
    while (i<=q && j<=r) {
      if (a[i] <= a[j]) {
        tmp[k++] = a[i++]; // i++等于i:=i+1
      } else {
        tmp[k++] = a[j++];
      }
    }

    // 判断哪个子数组中有剩余的数据
    int start = i;
    int end = q;
    if (j <= r) {
      start = j;
      end = r;
    }

    // 将剩余的数据拷贝到临时数组tmp
    while (start <= end) {
      tmp[k++] = a[start++];
    }

    // 将tmp中的数组拷贝回a[p...r]
    for (i = 0; i <= r-p; ++i) {
      a[p+i] = tmp[i];
    }
  }
def quick_sort(array, left, right):
	if left < right:
		index = partion(array, left, right)
		quick_sort(array, left, index)
		quick_sort(array, right, index + 1)
def partition(array, left, right):
	key = array[left]
	while left < right:
		while left < right and array[right] >= key:
			right-=1
		if left < right:
			array[left] = array[right]
		while left < right and array[left] <= key:
			left+=1
		if left < right:
			array[right] = array[left]
	array[left] = key
	return left

课程13:线性排序

复杂度为线性的O(n),不涉及比较的操作。桶排序,计数排序,基数排序。

课程14:排序的优化

针对快速排序——因为选择点的原因,最差情况为O(n2),可以采用分区算法;

1. 三数取中法; 2. 随机发;

课程15-16:二分法

// 二分查找的递归实现
public int bsearch(int[] a, int n, int val) {
  return bsearchInternally(a, 0, n - 1, val);
}

private int bsearchInternally(int[] a, int low, int high, int value) {
  if (low > high) return -1;

  int mid =  low + ((high - low) >> 1);//这里注意优先级以及不能直接加数据溢出的问题
  if (a[mid] == value) {
    return mid;
  } else if (a[mid] < value) {
    return bsearchInternally(a, mid+1, high, value);
  } else {
    return bsearchInternally(a, low, mid-1, value);
  }
}

局限性:顺序列表依赖数组;针对有序数据;数据量太小不适合;数据量太大因为空间不连续也不适合;

二分法和牛顿迭代求平方根“:

def sqrt_binary(num):
	x=sqrt(num)
	y=num/2.0
	low=0.0
	up=num*1.0
	count=1
	while abs(y-x)>0.00000001:
		print count,y
		count+=1		
		if (y*y>num):
			up=y
			y=low+(y-low)/2
		else:
			low=y
			y=up-(up-y)/2
	return y
def sqrt_newton(num):
	x=sqrt(num)
	y=num/2.0
	count=1
	while abs(y-x)>0.00000001:
		print count,y
		count+=1
		y=((y*1.0)+(1.0*num)/y)/2.0000
	return y

--------------------- 
作者:ycf74514 
来源:CSDN 
原文:https://blog.csdn.net/ycf74514/article/details/48996383 
版权声明:本文为博主原创文章,转载请附上博文链接!

 

你可能感兴趣的:(极客时间-数据结构与算法之美)