算法总结二

对于遍历和递归两种方式求解问题:

递归的形式大概如下:

public int GetResult(int n){

     if(n==0){           //递归退出条件
        return 0;          //这里返回的是当n=0的时候得到的值,而不是所有值的和

     }
    for( i...){             //遍历每一种可能

           GetResult(n-i);
   }

   return  s1+s2+s3;         //这里返回的是所有可能相加得到的值的和

}

遍历的形式如下:
public int GetResult(int n){
          while(true){

                 if(){

                          break;            //跳出循环
                   }

                for( i...){             //如果这里有多个可能性,那么要for遍历下各种可能性

                  ....

                 n=n-i;
               }

           }

}


动态规划:

和分治法相似,分治法是将问题划分成互不相交的子问题求解,递归求解子问题,再将解组合起来,动态规划是子问题有重叠的情况,两个不同的子问题下面有求同一个小问题,动态规划把重叠的小问题只求解一次保存在表格中,


贪心算法:

它在每一步做出当时看上去最优的解,通过做出局部最优解来得到全局最优解


对于实时的排队系统,虽然用数组也可以,但是考虑溢出问题和增加删除后的数据移动,很不方便

因此数组的长度是分配后不变的,线性表的长度是动态的


数据的存储形式分为顺序存储和链式存储。

int count=1;

while(count<n)

{

count=count*2;

}

这里由于2的x次幂=n所以x=log2(n)因此时间复杂度为O(logn)


一个线性表插入元素的时候,从最后一位到当前插入点遍历,i减减,每个元素向后移动一位,如果是i++;那么向后移动的话会把后面的元素覆盖掉,所以从后向前移动


数组和链表的区别:

                   数组                                                                       链表

               

对于插入和删除一个元素时,数组找到第i个元素只需一步,但是插入和删除需要把后面的全部移动一位,所以时间复杂度为O(n)

链表找到第i个元素必须从第一个元素开始向后找,所以时间复杂度为O(n)找到后插入删除操作只需一步

但是插入删除频繁时链表的优势就明显了

因此对于读取查找频繁操作,用数组这种顺序存储结构,对于插入删除频繁,用链表


对一个排序过的二叉树,进行中序遍历可以得到有序的序列,我们叫做二叉排序树


二分查找:传入数组长度,找到起点和终点,计算中点,如果目标值比中点的值大,把中点设为起点,递归后半段,否则把中点设为终点递归前半段。

缺点:必须是排序过的数组才能进行二分查找,对于频繁插入和删除需要继续排序,所以麻烦

插值查找:关键字分布比较均匀,插值查找比折半好一些。


冒泡排序:假设数组大小为9,比较9和8的值,把小的放到8号上,比较8和7的值,把小的放到7号上,循环到2和1上,这样第一次循环把最小的数放到了1号为,第二遍遍历,继续比较9和8的值。。。直到比较3和2的值,把倒数第二小的数放到2号位,这样一直遍历

for(int i=0;i<length;i++){

   for(int j=length;j>=i;j++){

      if(str[j]<str[j-1]){

      change(j,j-1);

      }

}  }

2层for循环,所以时间复杂度O(n2)


简单选择排序:不像冒泡只要小于就交换,它是在第一层for循环里面设了一个最小值,取出第二层遍历的最小值,交换

希尔排序:


将待排序数组按照步长gap进行分组,然后将每组的元素利用直接插入排序的方法进行排序;每次将gap折半减小,循环上述操作;当gap=1时,利用直接插入,完成排序。

时间复杂度O(n2/3)不稳定

堆排序任意的叶子节点小于(或大于)它所有的父节点。对此,又分为大顶堆和小顶堆,大顶堆要求节点的元素都要大于其孩子,小顶堆要求节点元素都小于其左右孩子,两者对左右孩子的大小关系不做任何要求。

构造大/小顶堆:有n个数,那么有n/2个根节点,让根节点大/小于左右孩子节点,最上面节点当然是最大/小的

重建堆:构建好后,把最顶端的节点移除,把序号最后的节点放到最顶端的节点,重新构造大/小顶堆

时间复杂度O(nlogn)

归并排序:


归并排序分为拆分和合并,拆分把序列一直对半拆,拆到只剩1个元素为止,合并是把两个对半序列合成排序后的序列,合并的时候这样合:比较左序列第一个和右序列第一个(假如左边的小),把左边第一个放到另一个数组的第一位,继续比较左序列第二个和右序列第一个,把小的放到另一个数组第二位。。这样循环

时间复杂度O(nlogn)

快速排序:

  1. 从序列当中选择一个基准数(pivot)
    在这里我们选择序列当中第一个数最为基准数
  2. 将序列当中的所有数依次遍历,比基准数大的位于其右侧,比基准数小的位于其左侧
  3. 重复步骤1.2,直到所有子集当中只有一个元素为止
     //快速排序的思想:取出第一个数,比该数小放左边,比该数大放右边,分成左右两个序列,递归
            static List<int> GetResult(List<int> list)
            {
                //终止条件,list只有1个数的时候推出循环
                if (list.Count==1)
                {
                    return list;
                }
                List<int> list3 = new List<int>();
                List<int> list4 = new List<int>();
                List<int> list5 = new List<int>();
                List<int> list6 = new List<int>();
                List<int> list7 = new List<int>();
                int fistNum = list[0];
                //小于第一个元素放左边的list,大于放右边的序列
                for (int i = 1; i < list.Count; i++)
                {
                    if (list[i] < fistNum)
                    {
                        list3.Add(list[i]);
                    }
                    else
                    {
                        list4.Add(list[i]);
                    }
                }
                //这里主要是保证不能让最终得到的左边或右边序列为空序列,因为如果为空序列,
                //递归时候return回去会发现左边序列或右边序列一直是一个空序列无限递归
                if (list3.Count==0)
                {
                    list3.Add(fistNum);
                }else if (list4.Count == 0)
                {
                    list4.Add(fistNum);
                }
                else
                {
                    list3.Add(fistNum);
                }
                //25, 15, 84, 65, 24, 74, 25, 15, 56, 28, 74, 55
                list5 = GetResult(list3);
                list6 = GetResult(list4);
                for (int i = 0; i < list6.Count; i++)
                {
                    list5.Add(list6[i]);
                }
                return list5;
            }

发现这样的代码不好,因为new了许多的list,那么排序数多的话,这么多的list比较消耗资源。我们可以把参数加上起始位置和终止位置,那么一直操作一个序列,在起始位置和终止位置中互换元素

//快速排序的思想:取出第一个数,比该数小放左边,比该数大放右边,分成左右两个序列,递归
        static void GetResult(List<int> list,int start,int end)
        {
            //终止条件,退出循环
            if (start==end)
            {
                return;
            }
            //
            int first = list[start];
            int firstIndex = start;
            for (int i=start+1;i<=end;i++)
            {
                if (list[i]<first)
                {
                    //如果第i个数更小,,把firstIndex到第i-1个数向后移动一位,第i个数移动到firstIndex位置,firstIndex设为第firstIndex+1个位置
                    //这里犯错是第一如果小于,不应该交换两个数而是移动它们,第二firstIndex设为第firstIndex+1个位置,而不是firstIndex和i进行交换
                    int num = list[i];  //这里很重要,从后向前移动数,那么最后一位要拿变量保存起来
                    for (int j=i-1;j>=firstIndex;j--)
                    {
                        list[j + 1] = list[j];
                    }
                    list[firstIndex] = num;
                    firstIndex ++;
                }
            }
            //25, 15, 84, 65, 24, 74, 25, 15, 56, 28, 74, 55
            GetResult(list, start, firstIndex);
            //要对后半段进行递归,那么firstIndex + 1是否超过list长度要做判断
            if (firstIndex == end)
            {

            }
            else
            {
                GetResult(list, firstIndex + 1, end);
            }
        }

Paitition算法:

我这里应该把快速排序算法弄错了,把小于第一个数的元素放前面,大于第一个数的所有元素放后面,使用这样的方法,叫做partition算法,上面的代码我是通过把数组移动的方式实现互换的,实际上partition算法没有移动元素而是直接交换的。

partition算法不仅用于快速排序,还用于在无序数组中寻找第K大的值

下面的图片和内容摘自:http://blog.jobbole.com/105219/

第一种paritition算法:

它的思想是取第一个数为目标数,用pos记录比目标数大的元素起始位置(刚开始为1),从第二个数开始遍历,找比目标数小的数,找到,pos++,互换比目标数大的起始位置的数和比目标数小的数。


int partition(vector<int>&arr, int begin, int end){
    int pivot = arr[begin];
    // Last position where puts the no_larger element.
    int pos = begin;
    for(int i=begin+1; i!=end; i++){
        if(arr[i] <= pivot){
            pos++;
            if(i!=pos){
                swap(arr[pos], arr[i]);
            }
        }
    }
    swap(arr[begin], arr[pos]);
    return pos;
}

这种实现思路比较直观,但是其实并不高效。从直观上来分析一下,每个小于pivot的值基本上(除非到现在为止还没有遇见大于pivot的值)都需要一次交换,大于pivot的值(例如上图中的数字9)有可能需要被交换多次才能到达最终的位置。

如果我们考虑用 Two Pointers 的思想,保持头尾两个指针向中间扫描,每次在头部找到大于pivot的值,同时在尾部找到小于pivot的值,然后将它们做一个交换,就可以一次把这两个数字放到最终的位置。一种比较明智的写法如下:

第二种partition算法:


使用变量i和j和target保存从左向右和从右向左的移动坐标和目标数,从右向左找比第一个数小的,有就把第i个数设为该数,i++,再从左向右找比第一个数大的,有就把第j个数设为该数,最后退出循环把第i个数设为target

这段代码也即快速排序采用的代码,也即第二种partition算法,比第一种高效。直观上来看,赋值操作的次数不多,比前面单向扫描的swap次数都少,效率应该会更高

 static  void quick_sort(int[] arr, int left, int right)
        {
            if (left < right)
            {
                int i = left, j = right, target = arr[left];
                while (i < j)
                {
                    while (i < j && arr[j] > target)
                        j--;
                    if (i < j)
                        arr[i++] = arr[j];

                    while (i < j && arr[i] < target)
                        i++;
                    if (i < j)
                        arr[j--] = arr[i];
                }
                arr[i] = target;
                quick_sort(arr, left, i - 1);
                quick_sort(arr, i + 1, right);
            }
        }

该算法的时间复杂度O(nlogn)



评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值