分类

类型:
不限 游戏开发 计算机程序开发 Android开发 网站开发 笔记总结 其他
评分:
不限 10 9 8 7 6 5 4 3 2 1
原创:
不限
年份:
不限 2018 2019

技术文章列表

  • 中山大学智慧健康服务平台应用开发-Broadcast使用

    一、实验题目中山大学智慧健康服务平台应用开发
    二、实现内容2.1 Broadcast 使用2.1.1 实验目的
    掌握 Broadcast 编程基础
    掌握动态注册 Broadcast 和静态注册 Broadcast
    掌握Notification 编程基础
    掌握 EventBus 编程基础

    2.1.2 实验内容在之前的基础上,实现静态广播、动态广播两种改变Notification 内容的方法。
    要求在启动应用时,会有通知产生,随机推荐一个食品

    点击通知跳转到所推荐食品的详情界面

    点击收藏图标,会有对应通知产生,并通过Eventbus在收藏列表更新数据

    点击通知返回收藏列表

    实现方式要求:启动页面的通知由静态广播产生,点击收藏图标的通知由动态广播产生。
    2.1.3 验收内容
    静态广播:启动应用是否有随机推荐食品的通知产生。点击通知是否正确跳转到所推荐食品的详情界面
    动态广播:点击收藏后是否有提示食品已加入收藏列表的通知产生。同时注意设置launchMode。点击通知是否跳转到收藏列表
    Eventbus:点击收藏列表图标是否正确添加食品到收藏列表。每点击一次,添加对应的一个食品到收藏列表并产生一条通知

    三、实验结果3.1 实验截图下图为打开app后,产生一个推荐食品的通知

    下图为点击该通知,会跳转至食物详情页面。点击收藏按钮时,产生收藏的通知

    下图为点击收藏通知,跳转至收藏列表页面

    3.2 实验步骤以及关键代码3.2.1 利用静态广播实现今日推荐功能在AndroidManifest.xml注册静态广播接受方其中StaticReceiver为类名
    <receiver android:name=".StaticReceiver"> <intent-filter> <action android:name="com.example.asus.health.MyStaticFilter" /> </intent-filter> </receiver>
    实现StaticReceiver类,重构onReceive函数其中要根据intent的action来确定是否接受该广播的内容,来实现功能,而需要实现的包括一个notification的弹出以及点击它跳转到详情页面。
    notification部分由builder的设置函数来设置名字,内容,等等,由NotificationManager来发出该notification。
    点击后跳转的功能则需要给builder设置一个ContentIntent,这个intent为PeddingIntent,即不会马上跳转,而是需要等待用户的操作。它的构造函数传递了一个普通的intent,而这个intent是携带了所需的数据来生成详情页面。
    public void onReceive(Context context, Intent intent) { if (intent.getAction().equals(STATICACTION)){ Bundle bundle = intent.getExtras(); //TODO:添加Notification部分 Notification.Builder builder = new Notification.Builder(context); //跳回主页面 Intent intent2 = new Intent(context,Details.class); Bundle bundle2 = new Bundle(); String s[] = new String [5]; s[0] = ((MyCollection)bundle.getSerializable("collect")).getName(); s[1] = ((MyCollection)bundle.getSerializable("collect")).getMaterial(); s[2] = ((MyCollection)bundle.getSerializable("collect")).getType(); s[3] = ((MyCollection)bundle.getSerializable("collect")).getContent(); s[4] = ((MyCollection)bundle.getSerializable("collect")).getIs_star()?"yes":"no"; bundle2.putStringArray("msg",s); intent2.putExtras(bundle2); PendingIntent contentIntent = PendingIntent.getActivity( context, 0, intent2, PendingIntent.FLAG_UPDATE_CURRENT); //对Builder进行配置 builder.setContentTitle("今日推荐") //设置通知栏标题:发件人 .setContentText(((MyCollection)bundle.getSerializable("collect")).getName()) //设置通知栏显示内容:短信内容 .setTicker("您有一条新消息") //通知首次出现在通知栏,带上升动画效果的 .setSmallIcon(R.mipmap.empty_star) //设置通知小ICON 空星 .setContentIntent(contentIntent) //传递内容 .setAutoCancel(true); //设置这个标志当用户单击面板就可以让通知将自动取消 //获取状态通知栏管理 NotificationManager manager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); //绑定Notification,发送通知请求 Notification notify = builder.build(); manager.notify(0,notify); } }
    在FoodList主页面onCreat时生成广播注意action的字符串要与上面的Reciver的相同,不然无法正确接受广播,随机数则是返回一个0到n-1的整数表示随机生成一个推荐食物,然后将所需数据放入intent,通过sendBroadcast函数发送该广播。
    //打开应用时,发送一个静态广播 private void boardcastforOpen(int n){ final String STATICACTION = "com.example.asus.health.MyStaticFilter"; Random random = new Random(); int num = random.nextInt(n); //返回一个0到n-1的整数 Intent intentBroadcast = new Intent(STATICACTION); //定义Intent Log.i("se",getPackageName()); Bundle bundles = new Bundle(); bundles.putSerializable("collect", data2.get(num)); intentBroadcast.putExtras(bundles); sendBroadcast(intentBroadcast); }
    3.2.2 利用动态广播实现收藏信息提示实现广播接受器DynamicReceiver类与静态Receiver的实现过程差不多,一样是实现builder,然后放置peddingIntent,这里就不再重复放代码。唯一的不同点在于,它所要跳回的是收藏夹页面,即FoodList主页面,这里要对intent设置flag,否则无法在foodlist中get到新的intent。
    //跳回收藏夹 Intent intent2 = new Intent(context,FoodList.class); Bundle bundle2 = new Bundle(); bundle2.putString("tag","collect"); intent2.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_SINGLE_TOP); intent2.putExtras(bundle2);
    对详情页面的收藏事件进行处理在监听器中设置发送广播的intent,当按下收藏后会发出广播,并传递参数。其中还使用了eventbus来传递收藏的数据。
    //处理收藏按钮 final ImageView collect_but = findViewById(R.id.collect); collect_but.setOnClickListener(new View.OnClickListener(){ @Override public void onClick(View v){ temp.setIs_collected(true); Toast.makeText(Details.this, "已收藏",Toast.LENGTH_SHORT).show(); EventBus.getDefault().post(new MessageEvent(temp)); //发送广播 Intent intentBroadcast = new Intent(); intentBroadcast.setAction(DYNAMICACTION); sendBroadcast(intentBroadcast); } });
    在FoodList注册动态接收器以及注销动态接收器注意分别要在onCreate函数以及onDestroy函数中实现注册与注销 。
    3.2.3 使用EventBus来实现数据的传输在这一点上,要改进上一周实验的代码,不再需要点击返回按钮利用setResult以及onActivityResult两个函数来返回信息。而是通过eventbus的订阅发布模式。
    在FoodList来注册订阅者,订阅消息。而在Detail来发布信息。其中onMessageEvent函数用于收到发布消息后,来调用之前的接口函数刷新列表。
    发布消息就是上面点击收藏按钮后 EventBus.getDefault().post(new MessageEvent(temp));
    //注册订阅者(注册收藏列表所在Activity为订阅者) EventBus.getDefault().register(this); @Subscribe(threadMode = ThreadMode.MAIN) public void onMessageEvent(MessageEvent event) { Log.i("hello","this is eventbus."); MyCollection mc = event.getCollection(); refreshList(mc,simpleAdapter); }
    3.2.4 从详情跳转回收藏夹这里由于收藏夹FoodList为经常返回的页面,故这里使用了android:launchMode=”singleInstance”,即不让它重复创建新的活动。
    所以再get我的返回intent时是拿不到新的intent的,这里需要重写onNewIntent函数,而且接收新的intent要在onResume中。
    这里要求要显示收藏夹页面,所以要将食物列表隐藏起来。
    @Override protected void onNewIntent(Intent intent){ super.onNewIntent(intent); setIntent(intent); } @Override protected void onResume(){ super.onResume(); //处理跳转 Bundle bundle=this.getIntent().getExtras(); if(bundle != null) { String str = bundle.getString("tag"); Log.i("resume2",str); if (str.equals("collect")) { findViewById(R.id.recyclerView).setVisibility(View.GONE); findViewById(R.id.listView).setVisibility(View.VISIBLE);//设置Favourite可见 tag_for_foodlist = false; f_but.setImageResource(R.mipmap.mainpage); } } }
    3.3 实验遇到的困难以及解决思路3.3.1 在安卓8.0版本中无法使用静态接收器,发送广播后,无法成功接收方法一:解决这个问题,需要给receiver设置component,给予它的包名以及类名。
    intent.setComponent(new ComponentName(getPackageName(),getPackageName()+".xxxxReceiver"));
    方法二:下载新的虚拟机,使用安卓7.0版本,则可以顺利接收静态广播。
    3.3.2 使用EventBus时候,FoodList主页面无法得到post的信息我按部就班地在Detail页面收藏按钮post,在FoodList订阅消息却毫无反应。首先,我认为是我的接收函数写错了,没有订阅到信息。通过Log.i发现确实没有进入到onMessageEvent函数中,于是对这个问题进行了查阅。网上有推荐使用stickyPost的,怀疑原因出在信息接收发生在创建接收者之前,但显然与函数执行顺序不符,它是先来到了主页面,所以必然创建了receiver。
    经过大半个小时的查找发现是,post传的参数错误,并没有生成MessageEvent,而是错误地直接传递了数据包。
    //错误EventBus.getDefault().post(temp);//正确EventBus.getDefault().post(new MessageEvent(temp));
    3.3.3 从收藏通知返回主页面时候,出现无法拿到intent的情况由于我是在动态接收方的builder绑定了Peddingintent,当点击通知,应该要返回这个intent到主页面,然而主页面所获取的intent是空值。这一点让我怀疑了很久,问了同学才得知,这是声明了singleInstance的问题。
    比如说在一个应用中A activity 跳转至 B activity 在跳转至 C activity 然后C做了一定的操作之后再返回A 界面。这样在A activity的启动模式设置为singleTask后。C界面跳转至A界面时,就会去判断栈内是否有改Activity实例,如果有就直接执行A界面的onNewIntent()方法,我们就可以把逻辑处理放在改生命周期方法中,如果没有就会走Activity的oncrate方法去创建实例。
    所以这里需要重写onNewIntent来获取新的intent,而不是直接传递旧intent导致错误。
    @Override protected void onNewIntent(Intent intent){ super.onNewIntent(intent); setIntent(intent); }
    四、实验思考及感想这次实验需要在安卓8.0与安卓7.0之间权衡,有些属性方法已经在8.0版本出现了变化,所以当使用错误,出现奇怪的现象时,第一步先检查自己的代码逻辑有否问题,第二步就是要查阅是否存在版本的兼容性问题产生了这些错误。这次作业就是如此,关于广播的实现,个人还是喜欢动态广播,不需要再静态注册在manifest中,代码也更加简便。
    对于不同活动之间的传输,使用EventBus比之前的intent更加方便,减轻了耦合性,不用经常记住,哪个intent返回哪里,所以这次我也修改了不少前面实验使用intent的代码。除此之外,充分理解信息传输还需要理解一下活动的存活过程,什么时候调用onCreat,什么时候使用onResume。
    2 留言 2019-07-17 23:38:40 奖励17点积分
  • 基于WinInet实现的HTTP文件下载 精华

    背景之前写过的网络数据传输的小程序都是基于Socket去写的,所以,如果要用Socket传输数据到网站,还需要根据域名获取服务器的IP地址,然后再建立连接,传输数据。虽然,Socket也可以实现网络传输,但是,总感觉不是很方便。所以,后来随着知识面的拓展,了解到Windows还专门提供了WinInet网络库,封装了比较简便的接口,去实现HTTP和FTP等传输协议的数据传输。
    本文就是基于WinInet网络库,实现通过HTTP传输协议下载文件功能的小程序。现在,就把开发过程的思路和编程分享给大家。
    主要函数介绍介绍HTTP下载文件使用到的主要的WinInet库中的API函数。
    1. InternetOpen介绍
    函数声明
    HINTERNET InternetOpen(In LPCTSTR lpszAgent,In DWORD dwAccessType,In LPCTSTR lpszProxyName,In LPCTSTR lpszProxyBypass,In DWORD dwFlags);
    参数lpszAgent指向一个空结束的字符串,该字符串指定调用WinInet函数的应用程序或实体的名称。使用此名称作为用户代理的HTTP协议。dwAccessType指定访问类型,参数可以是下列值之一:



    Value
    Meaning




    INTERNET_OPEN_TYPE_DIRECT
    使用直接连接网络


    INTERNET_OPEN_TYPE_PRECONFIG
    获取代理或直接从注册表中的配置,使用代理连接网络


    INTERNETOPEN_TYPE_PRECONFIG WITH_NO_AUTOPROXY
    获取代理或直接从注册表中的配置,并防止启动Microsoft JScript或Internet设置(INS)文件的使用


    INTERNET_OPEN_TYPE_PROXY
    通过代理的请求,除非代理旁路列表中提供的名称解析绕过代理,在这种情况下,该功能的使用



    lpszProxyName指针指向一个空结束的字符串,该字符串指定的代理服务器的名称,不要使用空字符串;如果dwAccessType未设置为INTERNET_OPEN_TYPE_PROXY,则此参数应该设置为NULL。
    lpszProxyBypass指向一个空结束的字符串,该字符串指定的可选列表的主机名或IP地址。如果dwAccessType未设置为INTERNET_OPEN_TYPE_PROXY的 ,参数省略则为NULL。
    dwFlags参数可以是下列值的组合:



    VALUE
    MEANING




    INTERNET_FLAG_ASYNC
    使异步请求处理的后裔从这个函数返回的句柄


    INTERNET_FLAG_FROM_CACHE
    不进行网络请求,从缓存返回的所有实体,如果请求的项目不在缓存中,则返回一个合适的错误,如ERROR_FILE_NOT_FOUND


    INTERNET_FLAG_OFFLINE
    不进行网络请求,从缓存返回的所有实体,如果请求的项目不在缓存中,则返回一个合适的错误,如ERROR_FILE_NOT_FOUND



    返回值成功:返回一个有效的句柄,该句柄将由应用程序传递给接下来的WinInet函数。失败:返回NULL。

    2. InternetConnect介绍
    函数声明
    HINTERNET WINAPI InternetConnect( HINTERNET hInternet, LPCTSTR lpszServerName, INTERNET_PORT nServerPort, LPCTSTR lpszUserName, LPCTSTR lpszPassword, DWORD dwService, DWORD dwFlags, DWORD dwContext);
    参数说明hInternet:由InternetOpen返回的句柄。lpszServerName:连接的ip或者主机名nServerPort:连接的端口。lpszUserName:用户名,如无置NULL。lpszPassword:密码,如无置NULL。dwService:使用的服务类型,可以使用以下

    INTERNET_SERVICE_FTP = 1:连接到一个 FTP 服务器上INTERNET_SERVICE_GOPHER = 2INTERNET_SERVICE_HTTP = 3:连接到一个 HTTP 服务器上
    dwFlags:文档传输形式及缓存标记。一般置0。dwContext:当使用回叫信号时, 用来识别应用程序的前后关系。返回值成功返回非0。如果返回0。要InternetCloseHandle释放这个句柄。

    3. HttpOpenRequest介绍
    函数声明
    HINTERNET HttpOpenRequest( _In_ HINTERNET hConnect, _In_ LPCTSTR lpszVerb, _In_ LPCTSTR lpszObjectName, _In_ LPCTSTR lpszVersion, _In_ LPCTSTR lpszReferer, _In_ LPCTSTR *lplpszAcceptTypes, _In_ DWORD dwFlags, _In_ DWORD_PTR dwContext);
    参数
    hConnect:由InternetConnect返回的句柄。
    lpszVerb:一个指向某个包含在请求中要用的动词的字符串指针。如果为NULL,则使用“GET”。
    lpszObjectName:一个指向某个包含特殊动词的目标对象的字符串的指针。通常为文件名称、可执行模块或者查找标识符。
    lpszVersion:一个指向以null结尾的字符串的指针,该字符串包含在请求中使用的HTTP版本,Internet Explorer中的设置将覆盖该参数中指定的值。如果此参数为NULL,则该函数使用1.1或1.0的HTTP版本,这取决于Internet Explorer设置的值。
    lpszReferer:一个指向指定了包含着所需的URL (pstrObjectName)的文档地址(URL)的指针。如果为NULL,则不指定HTTP头。
    lplpszAcceptTypes:一个指向某空终止符的字符串的指针,该字符串表示客户接受的内容类型。如果该字符串为NULL,服务器认为客户接受“text/*”类型的文档 (也就是说,只有纯文本文档,并且不是图片或其它二进制文件)。内容类型与CGI变量CONTENT_TYPE相同,该变量确定了要查询的含有相关信息的数据的类型,如HTTP POST和PUT。
    dwFlags:dwFlags的值可以是下面一个或者多个。



    价值
    说明




    INTERNET_FLAG_DONT_CACHE
    不缓存的数据,在本地或在任何网关。 相同的首选值INTERNET_FLAG_NO_CACHE_WRITE。


    INTERNET_FLAG_EXISTING_CONNECT
    如果可能的话,重用现有的连接到每个服务器请求新的请求而产生的InternetOpenUrl创建一个新的会话。 这个标志是有用的,只有对FTP连接,因为FTP是唯一的协议,通常在同一会议执行多个操作。 在Win 32 API的缓存一个单一的Internet连接句柄为每个HINTERNET处理产生的InternetOpen。


    INTERNET_FLAG -超链接
    强制重载如果没有到期的时间也没有最后修改时间从服务器在决定是否加载该项目从网络返回。


    INTERNET_FLAG_IGNORE_CERT_CN_INVALID
    禁用的Win32上网功能的SSL /厘为基础的打击是从给定的请求服务器返回的主机名称证书检查。 Win32的上网功能用来对付证书由匹配主机名和HTTP请求一个简单的通配符规则比较简单的检查。


    INTERNET_FLAG_IGNORE_CERT_DATE_INVALID
    禁用的Win32上网功能的SSL /厘为基础的HTTP请求适当的日期,证书的有效性检查。


    INTERNET_FLAG_IGNORE_REDIRECT_TO_HTTP
    禁用的Win32上网功能能够探测到这种特殊类型的重定向。 当使用此标志,透明的Win32上网功能允许对HTTP重定向的URL从HTTPS。


    INTERNET_FLAG_IGNORE_REDIRECT_TO_HTTPS
    禁用的Win32上网功能能够探测到这种特殊类型的重定向。 当使用此标志,透明的Win32上网功能允许从HTTP重定向到HTTPS网址。


    INTERNET_FLAG_KEEP_CONNECTION
    使用保持活动语义,如果有的话,给HTTP请求连接。 这个标志是必需的微软网络(MSN),NT LAN管理器(NTLM)和其他类型的身份验证。


    INTERNET_FLAG_MAKE_PERSISTENT
    不再支持。


    INTERNET_FLAG_MUST_CACHE_REQUEST
    导致一个临时文件如果要创建的文件不能被缓存。 相同的首选值INTERNET_FLAG_NEED_FILE。


    INTERNET_FLAG_NEED_FILE
    导致一个临时文件如果要创建的文件不能被缓存。


    INTERNET_FLAG_NO_AUTH
    不尝试HTTP请求身份验证自动。


    INTERNET_FLAG_NO_AUTO_REDIRECT
    不自动处理HTTP请求重定向只。


    INTERNET_FLAG_NO_CACHE_WRITE
    不缓存的数据,在本地或在任何网关。


    INTERNET_FLAG_NO_COOKIES
    不会自动添加Cookie标头的请求,并不会自动添加返回的Cookie的HTTP请求的Cookie数据库。


    INTERNET_FLAG_NO_UI
    禁用cookie的对话框。


    INTERNET_FLAG_PASSIVE
    使用被动FTP语义FTP文件和目录。


    INTERNET_FLAG_RAW_DATA
    返回一个数据WIN32_FIND_DATA结构时,FTP目录检索信息。 如果这个标志,或者未指定代理的电话是通过一个CERN,InternetOpenUrl返回的HTML版本的目录。


    INTERNET_FLAG_PRAGMA_NOCACHE
    强制要求被解决的原始服务器,即使在代理缓存的副本存在。


    INTERNET_FLAG_READ_PREFETCH
    该标志目前已停用。


    INTERNET_FLAG_RELOAD
    从导线获取数据,即使是一个本地缓存。


    INTERNET_FLAG_RESYNCHRONIZE
    重整HTTP资源,如果资源已被修改自上一次被下载。 所有的FTP资源增值。


    INTERNET_FLAG_SECURE
    请确保在使用SSL或PCT线交易。 此标志仅适用于HTTP请求。



    dwContext:OpenRequest操作的上下文标识符。

    4. InternetReadFile介绍
    函数声明
    BOOL InternetReadFile( __in HINTERNET hFile,__out LPVOID lpBuffer,__in DWORD dwNumberOfBytesToRead,__out LPDWORD lpdwNumberOfBytesRead);
    参数

    hFile[in]
    由InternetOpenUrl,FtpOpenFile, 或HttpOpenRequest函数返回的句柄.
    lpBuffer[out]
    缓冲器指针
    dwNumberOfBytesToRead[in]
    欲读数据的字节量。
    lpdwNumberOfBytesRead[out]
    接收读取字节量的变量。该函数在做任何工作或错误检查之前都设置该值为零

    返回值成功:返回TRUE,失败,返回FALSE

    程序设计原理该部分讲解下程序设计的原理以及实现的流程,让大家有个宏观的认识。原理是:

    首先,使用 InternetCrackUrl 函数分解URL,从URL中提取网站的域名、路径以及URL的附加信息等。关于 InternetCrackUrl 分解URL的介绍和实现,可以参考本站上的的 “URL分解之InternetCrackUrl” 这篇文章
    使用 InternetOpen 建立会话,获取会话句柄
    使用 InternetConnect 与网站建立连接,获取连接句柄
    设置HTTP的访问标志,使用 HttpOpenRequest 打开HTTP的“GET”请求
    使用 HttpSendRequest 发送访问请求
    根据返回的Response Header的数据中,获取将要接收数据的长度
    使用 InternetReadFile 接收数据
    关闭句柄,释放资源

    其中,上面的 8 个步骤中,要注意的就是第 6 步,获取返回的数据长度,是从响应信息头中的获取“Content-Length: ”(注意有个空格)这个字段的数据。
    编程实现1. 导入WinInet库#include <WinInet.h>#pragma comment(lib, "WinInet.lib")
    2. HTTP文件下载编程实现// 数据下载// 输入:下载数据的URL路径// 输出:下载数据内容、下载数据内容长度BOOL Http_Download(char *pszDownloadUrl, BYTE **ppDownloadData, DWORD *pdwDownloadDataSize){ // INTERNET_SCHEME_HTTPS、INTERNET_SCHEME_HTTP、INTERNET_SCHEME_FTP等 char szScheme[MAX_PATH] = { 0 }; char szHostName[MAX_PATH] = { 0 }; char szUserName[MAX_PATH] = { 0 }; char szPassword[MAX_PATH] = { 0 }; char szUrlPath[MAX_PATH] = { 0 }; char szExtraInfo[MAX_PATH] = { 0 }; ::RtlZeroMemory(szScheme, MAX_PATH); ::RtlZeroMemory(szHostName, MAX_PATH); ::RtlZeroMemory(szUserName, MAX_PATH); ::RtlZeroMemory(szPassword, MAX_PATH); ::RtlZeroMemory(szUrlPath, MAX_PATH); ::RtlZeroMemory(szExtraInfo, MAX_PATH); // 分解URL if (FALSE == Http_UrlCrack(pszDownloadUrl, szScheme, szHostName, szUserName, szPassword, szUrlPath, szExtraInfo, MAX_PATH)) { return FALSE; } // 数据下载 HINTERNET hInternet = NULL; HINTERNET hConnect = NULL; HINTERNET hRequest = NULL; DWORD dwOpenRequestFlags = 0; BOOL bRet = FALSE; unsigned char *pResponseHeaderIInfo = NULL; DWORD dwResponseHeaderIInfoSize = 2048; BYTE *pBuf = NULL; DWORD dwBufSize = 64 * 1024; BYTE *pDownloadData = NULL; DWORD dwDownloadDataSize = 0; DWORD dwRet = 0; DWORD dwOffset = 0; do { // 建立会话 hInternet = ::InternetOpen("WinInetGet/0.1", INTERNET_OPEN_TYPE_PRECONFIG, NULL, NULL, 0); if (NULL == hInternet) { Http_ShowError("InternetOpen"); break; } // 建立连接 hConnect = ::InternetConnect(hInternet, szHostName, INTERNET_DEFAULT_HTTP_PORT, szUserName, szPassword, INTERNET_SERVICE_HTTP, 0, 0); if (NULL == hConnect) { Http_ShowError("InternetConnect"); break; } // 打开并发送HTTP请求 dwOpenRequestFlags = INTERNET_FLAG_IGNORE_REDIRECT_TO_HTTP | INTERNET_FLAG_KEEP_CONNECTION | INTERNET_FLAG_NO_AUTH | INTERNET_FLAG_NO_COOKIES | INTERNET_FLAG_NO_UI; if (0 < ::lstrlen(szExtraInfo)) { // 注意此处的连接 ::lstrcat(szUrlPath, szExtraInfo); } hRequest = ::HttpOpenRequest(hConnect, "GET", szUrlPath, NULL, NULL, NULL, dwOpenRequestFlags, 0); if (NULL == hRequest) { Http_ShowError("HttpOpenRequest"); break; } // 发送请求 bRet = ::HttpSendRequest(hRequest, NULL, 0, NULL, 0); if (FALSE == bRet) { Http_ShowError("HttpSendRequest"); break; } // 接收响应的报文信息头(Get Response Header) pResponseHeaderIInfo = new unsigned char[dwResponseHeaderIInfoSize]; if (NULL == pResponseHeaderIInfo) { break; } ::RtlZeroMemory(pResponseHeaderIInfo, dwResponseHeaderIInfoSize); bRet = ::HttpQueryInfo(hRequest, HTTP_QUERY_RAW_HEADERS_CRLF, pResponseHeaderIInfo, &dwResponseHeaderIInfoSize, NULL); if (FALSE == bRet) { Http_ShowError("HttpQueryInfo"); break; }#ifdef _DEBUG printf("[HTTP_Download_ResponseHeaderIInfo]\n\n%s\n\n", pResponseHeaderIInfo);#endif // 从 中字段 "Content-Length: "(注意有个空格) 获取数据长度 bRet = Http_GetContentLength((char *)pResponseHeaderIInfo, &dwDownloadDataSize); if (FALSE == bRet) { break; } // 接收报文主体内容(Get Response Body) pBuf = new BYTE[dwBufSize]; if (NULL == pBuf) { break; } pDownloadData = new BYTE[dwDownloadDataSize]; if (NULL == pDownloadData) { break; } ::RtlZeroMemory(pDownloadData, dwDownloadDataSize); do { ::RtlZeroMemory(pBuf, dwBufSize); bRet = ::InternetReadFile(hRequest, pBuf, dwBufSize, &dwRet); if (FALSE == bRet) { Http_ShowError("InternetReadFile"); break; } ::RtlCopyMemory((pDownloadData + dwOffset), pBuf, dwRet); dwOffset = dwOffset + dwRet; } while (dwDownloadDataSize > dwOffset); // 返回数据 *ppDownloadData = pDownloadData; *pdwDownloadDataSize = dwDownloadDataSize; } while (FALSE); // 关闭 释放 if (NULL != pBuf) { delete[]pBuf; pBuf = NULL; } if (NULL != pResponseHeaderIInfo) { delete[]pResponseHeaderIInfo; pResponseHeaderIInfo = NULL; } if (NULL != hRequest) { ::InternetCloseHandle(hRequest); hRequest = NULL; } if (NULL != hConnect) { ::InternetCloseHandle(hConnect); hConnect = NULL; } if (NULL != hInternet) { ::InternetCloseHandle(hInternet); hInternet = NULL; } return bRet;}
    程序测试在main函数中,调用上述封装好的函数,下载文件进行测试。
    main函数为:
    int _tmain(int argc, _TCHAR* argv[]){ char szHttpDownloadUrl[] = "http://www.demongan.com/source/ccc/dasanxia/520.zip"; BYTE *pHttpDownloadData = NULL; DWORD dwHttpDownloadDataSize = 0; // HTTP下载 if (FALSE == Http_Download(szHttpDownloadUrl, &pHttpDownloadData, &dwHttpDownloadDataSize)) { return 1; } // 将下载数据保存成文件 Http_SaveToFile("http_downloadsavefile.zip", pHttpDownloadData, dwHttpDownloadDataSize); // 释放内存 delete []pHttpDownloadData; pHttpDownloadData = NULL; system("pause"); return 0;}
    测试结果:
    根据返回的Response Header知道,成功下载22761460字节大小的数据。

    查看目录,有22228KB大小的“http_downloadsavefile.zip”文件成功生成,所以,数据下载成功。

    总结基于WinInet库的HTTP下载文件原理并不复杂,但是,因为涉及较多的API,每个API的执行都需要依靠上一个API成功执行返回的数据。所以,要仔细检查。如果出错,也要耐心调试,根据返回的错误码,结合程序前后部分的代码,仔细分析原因。
    参考参考自《Windows黑客编程技术详解》一书
    3 留言 2018-12-20 17:46:07 奖励13点积分
  • C语言提高笔记

    C提高一 基本概念强化1、头文件函数声明
    分文件时,头文件防止头文件重复包含
    #pragma once//兼容C++编译器//如果是C++编译器,按照C标准编译#ifdef __cplusplusextern "c"{#endif//#ifdef __cplusplus}#endif数组作为函数参数会退化为一级指针:

    数组做函数参数时,应该把数组元素个数也传递给函数;
    形参中的数组,编译器把它当做指针处理,这是C语言的特色;
    实参中的数组,和形参中数组本质不一样;

    void print_array(int a[], int n)数据类型的本质:是固定内存大小的别名
    数据类型的作用:编译器预算对象(变量)分配的内存空间大小
    int a;//告诉C编译器分配4个字节的内存数据类型可以通过typedef起别名:typedef unsigned int u32;typedef struct student{ int a; char b;}STU;可以通过sizeof()测类型大小;Void类型 (无类型)

    如果函数没有参数,定义函数时,可以用void修饰:int fun(void);
    如果函数没有返回值,必须用void修饰:void fun(int a);
    不能定义void类型的普通变量,void a;//err,无法确定类型,不同类型分配空间不一样
    可以定义void 变量:void p;//ok,32位永远是4个字节,64位8字节
    void *p;万用指针,函数返回值,函数参数

    变量的本质:内存空间的别名
    必须通过数据类型定义变量
    变量相当于门牌号,内存相当于房间,通过门牌号找到房间,通过变量找到所对应的内存
    变量的赋值:1. 直接赋值 2. 间接赋值
    int a;a=100;//直接赋值int *p=0;p=&a;//指针指向谁,就把谁的地址赋值给指针*p=22;//间接赋值重点:没有变量,哪来内存,没有内存,哪里来内存首地址
    变量三要素(名称、大小、作用域),变量的生命周期
    内存四区模型

    栈区:系统分配空间,系统自动回收,函数内部定义的变量,函数参数,函数结束,其内部变量生命周期结束;
    堆区:程序员动态分配空间,由程序员手动释放,没有手动释放,分配的空间一直可用;
    静态区(全局区):(包括全局变量和静态变量,里面又分为初始化区和未初始化区,文字常量区:字符常量):整个程序运行完毕,系统自动回收;
    代码区,内存四区模型图

    栈区地址生长方向:地址由上往下递减;堆区地址生长方向:地址由下往上递增;数组buf,buf+1地址永远递增。

    函数调用模型
    程序各个函数运行流程(压栈弹栈,入栈出栈,先进后出)
    二 指针强化指针也是一种数据类型,指针变量也是一种变量,和int a本质是一样的
    1)指针变量也是一种变量,也有空间,32位程序大小为4个字节 int *p = 0x1122; 2)*操作符,*相当于钥匙,通过*可以找到指针所指向的内存区域 int a = 10; int *p = NULL; p = &a; //指针指向谁,就把谁的地址赋值给指针 *p = 22; //*放=左边,给内存赋值,写内存 int b = *p; //*放=右边,取内存的值,读内存 3)指针变量,和指向指向的内存是两个不同的概念 char *p = NULL; char buf[] = "abcdef"; //改变指针变量的值 p = buf; p = p + 1; //改变了指向变量的值,改变了指针的指向 *p = 'm'; //改变指针指向的内存,并不会影响到指针的值 4)写内存时,一定要确保内存可写 char *buf2 = "sadgkdsjlgjlsdk"; //文字常量区,内存不可改 //buf2[2] = '1'; //err间接赋值(*p)是指针存在最大意义
    1)间接赋值三大条件 a) 两个变量 b) 建立关系 c) 通过 * 操作符进行间接赋值 1) int a; int *p; //两个变量 p = &a; //建立关系 *p = 100; //通过 * 操作符进行间接赋值 2) int b; fun(&b); //两个变量之一:实参,给函数传参时,相当于建立关系 //p = &b void fun(int *p) //两个变量之一:形参参 { *p = 100; //通过 * 操作符进行间接赋值 } 2)如何定义合适类型的指针变量 //某个变量的地址需要定义一个怎么样类型的变量保存 //在这个类型的基础上加一个* int b; int *q = &b; int **t = &q;重要:如果想通过函数形参改变实参的值,必须传地址1、值传递,形参的任何修改不会影响到实参2、地址传递,形参(通过*操作符号)的任何修改会影响到实参用1级指针形参,去间接修改了0级指针(实参)的值。用2级指针形参,去间接修改了1级指针(实参)的值。用3级指针形参,去间接修改了2级指针(实参)的值。用n级指针形参,去间接修改了n-1级指针(实参)的值。 int a = 10; fun(a); //值传递 void fun(int b) { b = 20; } fun2(&a);//地址传递 void fun2(int *p) { *p = 20; //通过*操作内存 } int *p = 0x1122; void fun3(p);//值传递 void fun3(int *p) { p = 0x2233; } void fun4(&p);//地址传递 void fun4(int **p) { *p = 0xaabb; //通过*操作内存 }3、不允许向NULL和未知非法地址拷贝内存
    char *p = NULL; //给p指向的内存区域拷贝内容 strcpy(q, "1234"); //err //静态 char buf[100]; p = buf; strcpy(q, "1234"); //ok //动态 p = (char *)malloc(sizeof(char) * 10 ); strcpy(q, "1234"); //ok char *q = "123456"; strcpy(q, "abcd"); //?4、void *指针的使用
    void *p = 0x1122; //可以这么做,不建议,一般赋值为NULL char buf[1024] = "abcd"; p = (void *)buf; //指向 char printf("p = %s\n", (char *)p); //使用时转化为实际类型指针 int a[100] = { 1, 2, 3, 4 }; p = (void *)a; //指向 int int i = 0; for (i = 0; i < 4; i++) { //使用时转化为实际类型指针 int *tmp = (int *)p; printf("%d ", *(tmp+i)); printf("%d ", tmp[i]); printf("%d ", *( (int *)p + i ) ); } void * 常用于函数参数:memset(), memcpy()5、栈区返回变量的值和变量的地址区别
    int fun() { int a = 10; return a; } int *fun2() { int a = 10; return &a; } int *fun3() { static int a = 10; return &a; } int b = fun(); //ok, b的值为10 //也ok, p确实也保存了fun2()内部a的地址 //但是,fun2完毕,a就释放,p就指向未知区域 int *p = fun2(); //ok,fun3()中的a在全局区,函数运行完毕,a的空间不释放 int *q = fun3();6、.c -> 可执行程序过程 预处理:宏定义展开、头文件展开、条件编译,这里并不会检查语法 编译:检查语法,将预处理后文件编译生成汇编文件 汇编:将汇编文件生成目标文件(二进制文件) 链接:将目标文件链接为可执行程序 程序只有在运行才加载到内存(由系统完成),但是某个变量具体分配多大,是在编译阶段就已经确定了,换句话说,在编译阶段做完处理后,程序运行时系统才知道分配多大的空间,所以,很多时候说,这个变量的空间在编译时就分配(确定)了。
    7、指针做函数参数的输入输出特性输入:主调函数分配内存输出:被调用函数分配内存//结合内存四区模型图
    main() { char buf[100] = "123456"; fun1(buf); //输入 char *p = NULL; fun2(&p); //输出 //因为p在fun2()动态分配空间了,使用完毕应该释放 if(p != NULL) { free(p); p = NULL; } } void fun1(char *p /*in*/) { strcpy(p, "1234") } void fun2(char **p /*out*/) { char *tmp = malloc(100); strcpy(tmp, "1234"); *p = tmp; //间接赋值是指针存在最大意义,通过*操作内存 }8、变量内存的值和变量的地址int a = 0;a变量内存的值为0a变量的地址(&a)绝对不为0,只要定义了变量,系统会自动为其分配空间(一个合法不为0的地址)
    三 字符串操作1、字符串基本操作 1)字符串初始化 / C语言没有字符串类型,用字符数组模拟 C语言字符串以数字0,或字符 ‘\0’ 结尾,数字 0 和字符 ‘\0’ 等价 / char str1[100] = { ‘a’, ‘b’, ‘c’, ‘d’ }; //没赋值的自动以数字0填充 char str2[] = { ‘a’, ‘b’, ‘c’, ‘d’ }; //数组长度为4,结尾没有数字0 char str4[] = “abcdef”; //常用赋值方式,栈区 char p = “abcdef”; //文字常量区,内容不允许被修改 char buf = (char *)malloc(100); //堆区 strcpy(buf, “abcd”); //“abcd”拷贝到buf指向的内存区域中
    2)sizeof和strlen区别 //使用字符串初始化,常用 char buf8[] = “abc”; //strlen: 测字符串长度,不包含数字0,字符’\0’ //sizeof:测数组长度,包含数字0,字符’\0’ printf(“strlen = %d, sizeof = %d\n”, strlen(buf8), sizeof(buf8)); 3 4 char buf9[100] = “abc”; printf(“strlen = %d, sizeof = %d\n”, strlen(buf9), sizeof(buf9)); 3 100
    3)’\0’ 后面最好别跟数字,因为几个数字合起来有可能是一个转义字符 //\012相当于\n char str[] = “\0129”; printf(“%s aaaa\n”, str);
    4)字符’\0’, 数字0, 字符’0’, NULL的区别 a) 字符’\0’ ASCII码值为 0 的字符 字符’\0’ 和 数字 0 是等级的,’\0’中’\’是转义字符 char buf[100]; //下面是等级,在数组第10个位置设置字符串结束符 buf[10] = 0; buf[10] = ‘\0’; b) 字符’0’是字符串的某个字符内容为’0’, ASCII码值为 48 的字符 char buf[100]; buf[0] = ‘0’; //第0个字符为字符 ‘0’ c) NULL 标准头文件(stdio.h)的宏 其值为数字 0
    5)数组法、指针法操作字符串 char buf[] = “abdgdgdsg” char p = buf; //buf是数组首元素地址,它也是指针 for (i = 0; i < strlen(buf); i++) { //[ ] 和 操作是等价的,也是操作指针指向内存 printf(“%c “, buf[i]); //符合程序员习惯 printf(“%c “, p[i]); //符合程序员习惯 printf(“%c “, (p+i)); printf(“%c “, (buf + i)); } 注意:数组名也是指针,数组首元素地址,但是,它是一个只读常量 p++; //ok buf++; //err
    6)字符串拷贝函数 //成功为0,失败非0 //1 判断形参指针是否为NULL //2 最好不要直接使用形参 int my_strcpy(char dst, char src) { if (dst == NULL || src == NULL) { return -1; } //辅助变量把形参接过来 char to = dst; char from = src;
    //*dst = *src //dst++, src++ //判断 *dst是否为0, 为0跳出循环 while (*to++ = *from++) ; return 0;}2、项目开发常用字符串应用模型 1、利用strstr标准库函数找出一个字符串中substr出现的个数 1)do-while模型: char p = “11abcd111122abcd333abcd3322abcd3333322qqq”; int n = 0; do { p = strstr(p, “abcd”); if (p != NULL) { n++; //累计个数 //重新设置查找的起点 p = p + strlen(“abcd”); } else //如果没有匹配的字符串,跳出循环 { break; } } while (p != 0); //如果没有到结尾
    2)while模型: char p = “11abcd22222abcd33333abcd444444qqq”; int n = 0; while( (p = strstr(p, “abcd”)) != NULL ) { //能进来,肯定有匹配的子串 //重新设置起点位置 p = p + strlen(“abcd”); n++; if(p == 0)//如果到结束符 { break; } } printf(“n = %d\n”,n);
    3)函数封装实现 int my_strstr(char p,int n) { //辅助变量 int i = 0; char tmp = p; while((tmp = strstr(tmp, “abcd”)) != NULL) { //能进来,肯定有匹配的子串 //重新设置起点位置 tmp = tmp + strlen(“abcd”); i++; if(tmp == 0)//如果到结束符 { break; } } //间接赋值 n = i; return 0; } int main(void) { char p = “11abcd22222abcd33333abcd444444qqq”; int n = 0; int ret = 0; ret = my_strstr(p,&n); if(ret != 0) { return ret; } printf(“n = %d\n”,n); return 0;}
    2、两头堵模型 char *p = “ abcddsgadsgefg “; int begin = 0; int end = strlen(p) - 1; int n = 0; if(end < 0){ return; } //从左往右移动,如果当前字符为空,而且没有结束 while (p[begin] == ‘ ‘ && p[begin] != 0) { begin++; //位置从右移动一位 } //从右往左移动,如果当前字符为空 while (p[end] == ‘ ‘) { end—; //往左移动 } n = end - begin + 1; //非空元素个数 strncpy(buf, p + begin, n); buf[n] = 0;
    //如何证明strncpy()拷贝不会自动加字符串结束符'\0'char dst[] = "aaaaaaaaaaaaaaa";strncpy(dst, "123", 3);printf("dst = %s\n", dst); //dst = "123aaaaaaaaaaaa"四 const的使用1)const声明变量为只读 //const修饰的变量,定义时初始化 const int a = 10; //a = 100; //error int q = &a;
    q = 22; char buf[100] = “abcdef”;//从左往右看,跳过类型,看修饰那个字符 //如果是修饰,说明指针指向的内存不能改变 //如果是修饰指针变量,说明指针的指向不能改变,指针的值不能修改 const char p = buf; //类似于文字常量区 char p = “123445”; char const p = buf; //修饰,指针指向能变,指针指向的内存不能变 //p[0] = ‘1’; //error p = “123456”; //ok char const p1 = buf; //修饰指针变量,指针指向的内存,指针指向不能变 //p1 = “123456”; //error p1[0] = ‘1’; //ok const char * const p2 = buf; //p2, 只读
    2)如何引用另外.c中的const变量 extern const int a;不能再赋值,只能声明
    五 多级指针1)如何定义合适类型的指针变量 //某个变量的地址需要定义一个怎么样类型的变量保存 //在这个类型的基础上加一个 int b; int q = &b; //一级指针 int t = &q; //二级指针 int *m = &t; //三级指针
    2)二级指针做输出 输入:主调函数分配内存 输出:被调用函数分配内存 char *p1 = NULL; //没有分配内存 int len = 0; getMem(&p1, &len); //要想通过函数的形参改变实参的值,必须地址传递
    void getMem(char **p1 /*out*/, int *plen /*in*/){ //间接赋值,是指针存在最大的意义。 *p1 = malloc(100); *plen = 100;} 指针做参数输出特性3)二级指针做输入的三种内存模型 1、//指针数组,数组的每个元素都是指针类型 // [] 的优先级比 高,它是数组,每个元素都是指针类型(char ) char myArray[] = {“aaaaaa”, “ccccc”, “bbbbbb”, “111111”}; //char **p = {“aaaaaa”, “ccccc”, “bbbbbb”, “111111”}; //err void fun(int a[]); void fun(int a); // a[] 等价于 a void printMyArray(char myArray[], int num); // char 代表类型,myArray[]等价于 myArray // char myArray[] -> char myArray void printMyArray(char myArray, int num); void sortMyArray(char *myArray, int num); 如果排序,交换的是指针的指向,因为原来指针指向是文字常量区,文字常量区的内存一旦分配,内存就不能变。
    2、//二维数组 10行30列,10个一维数组a[30] //总共能容量10行字符串,这个用了 4 行 //每行字符串长度不能超过29,留一个位置放结束符:数字0 char myArray[10][30] = {“aaaaaa”, “ccccc”, “bbbbbbb”, “1111111111111”}; void printMyArray(char myArray[10][30], int num); void sortMyArray(char myArray[10][30], int num); //定义二维数组,不写第一个[ ]值有条件,必须要初始化 char a[][30] = {“aaaaaa”, “ccccc”, “bbbbbbb”, “1111111111111”};//ok char a[][30]; //err,定义时必须初始化
    二维数组的数组名代表首行地址(第一行一维数组的地址)首行地址和首行首元素地址的值是一样的,但是它们步长不一样首行地址+1,跳过一行,一行30个字节,+30首行首元素地址+1,跳过一个字符,一个字符为1个字节,+1sizeof(a): 有4个一维数组,每个数组长度为30,4 * 30 = 120sizeof(a[0]): 第0个一维数组首元素地址,相当于测第0个一维数组的长度:为30char b[30];&b代表整个一维数组的地址,相当于二维数组首行地址b代表一维数组首元素地址,相当于二维数组首行首元素地址&b 和 b 的值虽然是一样,但是,它们的步长不一样&b + 1: 跳过整个数组,+30b+1: 跳过1个字符,+1//不能通过 char ** 作为函数形参,因为指针+1步长不一样// char **,指针+1步长为 4 个字节// char a[][30],指针+1步长为 1 行的长度,这里为 30 个字节void printMyArray(char **buf, int num);3、int a[3];int *q = (int *)malloc(3 * sizeof(int)); //相当于q[3]//动态分配一个数组,每个元素都是char * //char *buf[3]int n = 3;char **buf = (char **)malloc(n * sizeof(char *)); //相当于 char *buf[3]if (buf = = NULL){ return -1;} for (i = 0; i < n; i++) { buf[i] = (char )malloc(30 sizeof(char)); }
    char **myArray = NULL;char **getMem(int num); //手工打造二维数组void printMyArray(char **myArray, int num);void sortMyArray(char **myArray, int num);void arrayFree(char **myArray, int num);第三种内存模型:
    char **getMem(int n){ int i = 0; char **buf = (char **)malloc(n * sizeof(char *)); //char *buf[3] if (buf == NULL) { return NULL; } for (i = 0; i < n; i++) { buf[i] = (char *)malloc(30 * sizeof(char)); char str[30]; sprintf(str, "test%d%d", i, i); strcpy(buf[i], str); } return buf;}void print_buf(char **buf, int n){ int i = 0; for (i = 0; i < n; i++) { printf("%s, ", buf[i]); } printf("\n");}void free_buf(char **buf, int n){ int i = 0; for (i = 0; i < n; i++) { free(buf[i]); buf[i] = NULL; } if (buf != NULL) { free(buf); buf = NULL; }}int main(void){ char **buf = NULL; int n = 3; buf = getMem(n); if (buf == NULL) { printf("getMem err\n"); return -1; } print_buf(buf, n); free_buf(buf, n); buf = NULL; printf("\n"); system("pause"); return 0;}1、一维数组的初始化 int a[] = { 1, 3, 5 }; //3个元素 int b[5] = { 1, 2, 3 }; //a[3], a[4]自动初始化为0 int c[10] = { 0 }; //全部元素初始化为0 memset(c, 0, sizeof(c)); //通过memset给数组每个元素赋值为0
    2、数组类型 int a[] = { 1, 3, 5 }; //3个元素 a: 数组首行首元素地址,一级指针 &a: 整个数组的首地址,二级指针
    首行首元素地址和首行(整个一维数组)地址值虽然是一样,但是它们的步长不一样a+1: 跳过1元素,一元素为4字节,步长4个字节&a+1: 跳过整个数组,整个数组长度为 3*4 = 12,步长 3 * 4 = 12sizeof(a): 传参为:数组首行首元素地址,测数组(int [3])的长度,3 * 4 = 12sizeof(a[0]): 传参为:数组首元素(不是地址),每个元素为int类, 4字节sizeof(&a):传参为:一维数组整个数组的地址(首行地址),编译器当做指针类型,4字节(重要)首行地址 --> 首行首元素地址(加*)&a:首行地址*&a -> a : 首行首元素地址//数组也是一种数据类型,类型本质:固定大小内存块别名//由元素类型和内存大小(元素个数)共同决定 int a[5] int[5]//可以通过typedef定义数组类型//有typedef是类型,没有typedef是变量typedef int ARRARY[5]; //定义了一个名字为ARRARY的数组类型//等价于typedef int (ARRARY)[5];//根据数组类型,定义变量//ARRARY的位置替代为d,去掉typedef,int d[5]ARRARY d; //相当于int d[5];3、指针数组(它是数组,每个元素都是指针) 1)指针数组的定义 //指针数组变量 //[]优先级比高,它是数组,每个元素都是指针(char ) char *str[] = { “111”, “2222222” };
    char **str = { "111", "2222222" }; //err2)指针数组做形参void fun(char *str[]);void fun(char **str); //str[] -> *str3)main函数的指针数组//argc: 传参数的个数(包含可执行程序)//argv:指针数组,指向输入的参数int main(int argc, char *argv[]);: demo.exe a b testint argc = 4char *argv[] = {"demo.exe", "a", "b", "test"}4、数组指针变量(它是指针变量,指向数组的指针) //定义数组变量 int a[10]; //有typedef:类型 //没有typedef:变量 1、根据数组类型,定义指针变量,数组指针变量 typedef int ARRARY[10]; //定义了一个名字为ARRARY的数组类型 //等价于typedef int (ARRARY)[10];
    ARRARY *p; //数组指针变量//编译会有警告,但不会出错,因为 a 和 &a的值一样//就算p = a这样赋值,编译器内部也会自动转换为 p = &a//不建议这么做p = a; //p 指向a数组,指向一维数组的指针p = &a; //如何操作数组指针变量 pint i = 0;for (i = 0; i < 10; i++){ (*p)[i] = i + 1; //p = &a //*p -> *(&a) -> a //(*p)[i] -> a[i]}2、直接定义数组指针变量(常用)//()[]同级,从左往右看//()有*,它是一个指针,[]代表数组//指向数组的指针变量,[]中的数字代表指针+1的步长int(*p)[10]; //p 指向a数组,指向一维数组的指针p = &a;3、先定义数组指针类型,再根据类型定义指针变量(常用)//和指针数组写法很类似,多了()//()和[]优先级一样,从左往右//()有指针,它是一个指针,[]//指向数组的指针,它有typedef,所有它是一个数组指针类型//数组指针类型,加上typedeftypedef int(*Q)[10];Q p; //根据类型定义变量,p是数组指针变量p = &a; //p指向数组a5、多维数组本质 1)二维数组初始化 int a1[3][4] = { {1, 2, 3, 4}, {5, 6, 7, 8}, {9, 10, 11, 12} }; int a2[3][4] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12 }; int a3[][4] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12 };
    2)内存中并不存在多维数组,多维数组在内存中都是线性存储int a[3][5] = { 0 };int *b = (int *)a;int i = 0;for(i = 0; i < 15; i++){ printf("%d ", b[i]);}printfA((int *)a,sizeof(a)/sizeof(a[0][0]));3)多维数组名//学会类比int b[5] = {0};b: 首行首元素地址, +1,跳 4 个字节&b:首行地址,+1,跳 4*5 = 20个字节//二维数组实际上就是 N 个一维数组//把二维数组第一个[]的值看做标志位,0 -> 2//第0个一维数组a[5] -> 第2个一维数组a[5]int a[3][5] = { 0 };a:二维数组首元素地址代表首行地址,相当于一维数组整个数组的地址,相当于上面的 &b,本来就是一个二级指针//(重要)首行地址 --> 首行首元素地址(加*)*a:首行首元素地址,相当于一维数组首元素地址,相当于上面的 ba + i -> &a[i]: 第i行地址//(重要)某行地址 --> 某行首元素地址(加*)*(a+i) -> *&a[i] -> a[i]: 第i行首元素地址//第i行j列元素的地址,某行首元素地址 + 偏移量*(a+i)+j -> a[i]+j -> &a[i][j]: 第i行j列元素的地址//第i行j列元素的值,第i行j列元素的地址的基础上(加 *)*(*(a+i)+j) -> a[i][j]: 第i行j列元素的值int a[3][5] = { 0 };sizeof(a): 二维数组整个数组长度,4 * 3 * 5 = 60sizeof(a[0]):a[0]为第0行首元素地址,相当于测第0行一维数组的长度:4 * 5 = 20sizeof(a[0][0]):a[0][0]为第0第0列元素(是元素,不是地址),测某个元素长度:4字节4)多维数组名,实际上是一个数组指针,指向数组的指针,步长为一行字节长度int a[3][5] = { 0 };//定义一个数组指针类型的变量int(*p)[5];//编译会有警告,但不会出错,因为 a 和 &a的值一样//但是&a代表整个二维数组的首地址//就算p = &a这样赋值,编译器内部也会自动转换为 p = a//不建议这么做p = &a;//a 本来就是第0个一维数组整个数组的地址,所以,无需加&p = a;5)二维数组做形参的三种形式//一维数组做函数参数退化为一级指针//二维数组(多维数组)做函数参数,退化为数组指针int a[3][5] = { 0 };void print_array1(int a[3][5]);//第一维的数组,可以不写//第二维必须写,代表步长,确定指针+1的步长 5*4void print_array2(int a[][5])//形参为数组指针变量,[]的数字代表步长void print_array3(int (*a)[5]);//a+1和二维数组的步长不一样//这里的步长为4//上面二维数组的步长为 5 * 4 = 20void print_array3(int **a); //err6、小结 typedef int A[10];//A:数组类型 A b;//int b[10],数组类型变量,普通变量 A *p;//数组类型定义数组指针变量
    typedef int (*P)[10];//数组指针类型P p;//数组指针变量int (*q)[10];//数组指针变量六 结构体1、结构体类型基本操作 1)结构体类型定义 //struct结构体关键字 //struct STU合起来才是类型名 //{}后面有个分号 struct Stu { char name[32]; char tile[32]; int age; char addr[50]; }; //通过typedef把struct Stu改名为Stu typedef struct Stu { int a; }Stu;
    2)结构体变量的定义//1)先定义类型,再定义变量,最常用struct Stu a;//全局变量、局部变量//2)定义类型的同时,定义变量struct _Stu{ char name[32]; char tile[32]; int age; char addr[50];}c;struct{ char name[32]; char tile[32]; int age; char addr[50];}e, f;3)结构体变量初始化//定义变量同时时初始化,通过{}struct Stu g = { "lily", "teacher", 22, "guangzhou" };4)变量和指针法操作结构体成员//变量法, 点运算符struct Stu h;strcpy(h.name, "^_^");(&h)->name//指针法, ->//结构体指针变量,没有指向空间,不能给其成员赋值struct Stu *p;p = &h;strcpy(p->name, "abc");(*p).name结构体也是一种数据类型,复合类型,自定义类型5)结构体数组
    //结构体类型 typedef struct Teacher { char name[32]; int age; }Teacher; //定义结构体数组,同时初始化 Teacher t1[2] = { { "lily", 18 }, { "lilei", 22 } }; //静态数组 Teacher t1[2] = {"lily", 18, "lilei", 22 }; int i = 0; for(i = 0; i < 2; i++) { printf(“%s, %d\n”, t1[i].name, t1[i].age);} //动态数组 Teacher *p = (Teacher *)malloc(3 * sizeof(Teacher)); if(p ==NULL) { return -1; } char buf[50]; int i; for(i = 0; i < 3; i++) { sprintf(buf,"name%d%d%d",i,i,i); strcpy(p[i].name,buf); p[i].age = 20 + i;}2、结构体赋值 //定义结构体类型是不要直接给成员赋值 //结构体只是一个类型,还没有分配空间 //只有根据其类型定义变量时,才分配空间,有空间后才能赋值 Teacher t1 = { “lily”, “teacher”, 18, “beijing” }; //相同类型的结构体变量,可以相互赋值 //把t1每个成员的内容逐一拷贝到t2对应的成员中 t1和t2没有关系 Teacher t2 = t1;
    3、结构体套指针
    1)结构体嵌套一级指针类型 typedef struct Teacher { char *name; int age; }Teacher; Teacher *p = NULL; p = (Teacher *)malloc(sizeof(Teacher)); p->name = (char *)malloc(30); strcpy(p->name,”lilei”); p->age = 22; 2)结构体嵌套二级指针类型 typedef struct Teacher { char *name; char **stu; int age; }Teacher; //1 Teacher t; //t.stu[3]; //char *t.stu[3];int n = 3; int i = 0; t.stu = (char **)malloc(n * sizeof(char *)); for(i = 0; i < n; i++) { t.stu[i] = (char *)malloc(30); strcpy(t.stu[i],”lily”); } //2 Teacher *p = NULL; //p->stu[3] p = (Teacher *)malloc(sizeof(Teacher)); //char *p->stu[3]p->stu = (char **)malloc(n * sizeof(char *));//3 Teacher *q = NULL; //Teacher *q[3] //q[i].stu[3] q = (Teacher *)malloc(sizeof(Teacher) * 3) for (i = 0; i < 3; i++) { //q[i].stu //(q+i)->stu q[i].stu = (char **)malloc(3 * sizeof(char *)); //char *stu[3] for(j = 0; j < 3; j++) { q[i].stu[j] = (char *)malloc(30); } }4、结构体做函数参数
    int getMem(Teacher **tmp, int n) { if(tmp == NULL) { return -1; } Teacher *p = (Teacher *)malloc(sizeof(Teacher) * 3); //Teacher q[3]; int i = 0; char buf[30]; for(i = 0; i < n; i++) { p[i].name = (char *)malloc(30); sprintf(buf,”name%d%d%d”, i, i, i); strycpy(p[i].name, buf); p[i].age = 20 + 2 * i;}*tmp = p;retrun 0; }5、浅拷贝和深拷贝 typedef struct Teacher { char name; int age; }Teacher; //结构体中嵌套指针,而且动态分配空间 //同类型结构体变量相互赋值 //不同结构体成员指针变量指向同一块内存 Teacher t1; t1.name = (char )malloc(30); strcpy(t1.name, “lily”); t1.age = 22;
    Teacher t2;t2 = t1;//深拷贝,人为增加内存,重新拷贝一下t2.name = (char *)malloc(30);strcpy(t2.name, t1.name);6、结构体偏移量(了解) //结构体类型定义下来,内部的成员变量的内存布局已经确定 typedef struct Teacher { char name[64]; //64 int age; //4 int id; //4 }Teacher;
    Teacher t1;Teacher *p = NULL;p = &t1;int n1 = (int)(&p->age) - (int)p; //相对于结构体首地址int n2 = (int)&((Teacher *)0)->age; //绝对0地址的偏移量7、结构体字节对齐(以空间换时间),详情请看《结构体字节对齐规则.doc》 原则1:数据成员的对齐规则(以最大的类型字节为单位)。原则2:结构体作为成员的对齐规则。 注意:

    结构体A所占的大小为该结构体成员内部最大元素的整数倍,不足补齐。不是直接将结构体A的成员直接移动到结构体B中 原则3:收尾工作结构体的总大小,也就是sizeof的结果,必须是其内部最大成员的整数倍,不足的要补齐。 struct A{ int a; double b; float c;};struct{ char e[2]; int f; int g; short h; struct A i;}B;//对齐单位 8 个字节sizeof(B) = 40//普通成员偏移量e: 2*0 = 0f: 4*1 = 4g: 4*2 = 8h: 2*6 = 12结构体起点坐标: 8*2 = 16//结构体成员偏移量a: 16 + 4*0 = 16b: 16 + 8*1 = 24c: 16 + 4*4 = 32
    七 文件7.1 基本概念7.1.1 文件分类 普通文件:存放在硬盘中的文件 设备文件:屏幕、键盘等特殊文件
    文本文件:ASCII文件,每个字节存放一个字符的ASCII码,打开文件看到的是文本信息二进制文件:数据按其在内存中的存储形式原样存放,打开文件看到的是乱码7.1.1文件缓冲区(了解) ANSI C(标准C语言库函数)标准采用“缓冲文件系统”处理数据文件。
    写文件(设备文件除外),并不会直接写到文件中,会先放在缓冲区,默认情况下,关闭文件或缓冲区满了才写到文件。如果没有关闭文件,缓冲区也没有满,可以通过程序正常结束,或者人为刷新缓冲区fflush(fd)来把缓冲区的内容写到文件中。缓冲区了解一下即可,增加缓冲区只是为了提高效率,减少频繁交互的次数,我们写程序基本上不用关心。7.2 读写文件步骤7.2.1 打开文件 //定义文件指针 FILE *fp = NULL; fopen(“c:\demo.txt”, “w+”); //“c:\demo.txt” windows有效 //“c:/demo.txt”: 文件路径,可以是绝对路径和相对路径 //“w+”: 打开权限,读写方式打开,文件不存在则创建,写内容时,会清空原来内容再写 //“r+”:读写方式打开,文件不存在则报错 fp = fopen(“./demo.txt”, “w+”); // 45度 “c:/demo.txt” linux windows都可用 if (fp == NULL) { perror(“fopen”); //打印错误信息 return; }
    默认情况下,VS, Qt相对路径说明:1)编译代码时,相对路径相对于工程目录2)直接点击可执行程序,相对路径相对于可执行程序//”C:\\Users” windows的写法//”C:/Users” Linux,windows都支持,建议”/”c语言中有三个特殊的文件指针无需定义、打开可直接使用:stdin: 标准输入 默认为当前终端(键盘)我们使用的scanf、getchar函数默认从此终端获得数据stdout:标准输出 默认为当前终端(屏幕)我们使用的printf、puts函数默认输出信息到此终端stderr:标准出错 默认为当前终端(屏幕)当我们程序出错或者使用: perror函数时信息打印在此终端fputc('a', stdout); //stdout -> 屏幕, 打印普通信息char ch;ch = fgetc(stdin); //std -> 键盘printf("ch = %c\n", ch);//fprintf(stderr, "%c", ch ); //stderr -> 屏幕, 错误信息fputc(ch, stderr);printf 标准输出sprintf 字符输出fprintf 文件输出7.2.2 读写文件 1、输出,即为写,把buf中的内容写到指定的文件中 2、输入,即为读,把文件中的内容取出放在指定的buf
    7.2.3 关闭文件 fclose(fp);
    if(fp != NULL){ fclose(fp); fp = NULL;}
    7.3 读写文件7.3.1 库函数的学习 1)包含所需头文件 2)函数名字 3)功能 4)参数 5)返回值
    7.3.2 按照字符读写文件:fgetc()、fputc() 1)写文件 char *str = “111abcdefg12345678”; int i = 0; for (i = 0; i < (int)strlen(str); i++) { //功能:往文件fp中写str[i],一个字符一个字符写 //参数:str[i]:写入文件的字符,fp:文件指针 //返回值:成功写入文件的字符,失败:-1 fputc(str[i], fp); }
    2)读文件char ch;//feof(fp)判断文件是否到结尾,已经到结尾返回值为非0,没有到结尾返回值为0while ( !feof(fp) ) //如果文件没有结尾{ //返回值:成功读取的字符 ch = fgetc(fp); printf("%c", ch);}7.3.4 按照行读写文件:fputs()、fgets() 1)写文件 char *buf[] = { “11111111\n”, “aaaaaaaaaaaa\n”, “bbbbbbbbbbbb\n” }; //指针数组 int i = 0; for (i = 0; i < 3; i++) { //功能:往文件fp写入一行内容buf[i] //参数:buf[i]:字符串首地址,fp:文件指针 //返回值:成功:0,失败:非0 fputs(buf[i], fp); }
    2)读文件char buf[512] = {0};//从文件中读取一行内容(以"\n"作为标志),放在buf中//一次最大只能读sizeof(buf)-1,如果小于sizeof(buf)-1,则按实际大小读取//然后在字符串结尾自动加上字符‘\0’(转换为C风格字符串)//返回值:成功:读出的字符串,失败:NULLif (fgets(buf, sizeof(buf), fp) != NULL) { printf("buf = %s", buf);}7.3.5 按照块读写文件:fread()、fwirte() typedef struct Stu { char name[50]; int id; }Stu; Stu s[3];
    1)写文件//写文件,按块的方式写//s:写入文件内容的内存首地址//sizeof(Stu):块数据的大小//3:块数, 写文件数据的大小 sizeof(Stu) *3//fp:文件指针//返回值,成功写入文件的块数目,不是数据总长度int ret = fwrite(s, sizeof(Stu), 3, fp);printf("ret = %d\n", ret);2)读文件//读文件,按块的方式读//s:放文件内容的首地址//sizeof(Stu):块数据的大小//3:块数, 读文件数据的大小 sizeof(Stu) *3//fp:文件指针//返回值,成功读取文件内容的块数目,不是数据总长度int ret = fread(s, sizeof(Stu), 3, fp);printf("ret = %d\n", ret);7.3.6 按照格式化进行读写文件:fprintf()、fscanf() 1)写文件 //格式化写文件 int a = 250; int b = 10; int c = 20; //和printf()用法一样,只是printf是往屏幕(标准输出)写内容 //fprintf往指定的文件指针写内容 //返回值:成功:写入文件内容的长度,失败:负数 fprintf(fp, “Tom = %d, just like %d, it is %d”, a, b, c);
    2)读文件int a, b, c;fscanf(fp, "Tom = %d, just like %d, it is %d", &a, &b, &c);printf("a = %d, b = %d, c = %d\n", a, b, c);7.3.7 随机读写 //文件光标移动到文件结尾 //SEEK_SET:文件开头 //SEEK_CUR:文件当前位置 //SEEK_END:文件结尾 fseek(fp, 0, SEEK_END);
    //获取光标到文件开头文件的大小ftelllong size = ftell(fp);//文件光标恢复到开始位置rewind(fp);typedef struct Stu{ char name[50]; int id;}Stu;Stu tmp; //读第3个结构体//假如文件中写了三个结构体//从起点位置开始,往后跳转2个结构体的位置fseek(fp, 2*sizeof(Stu), SEEK_SET);//从结尾位置开始,往前跳转一个结构体的位置//fseek(fp, -1 * (int)sizeof(Stu), SEEK_END);int ret = 0;ret = fread(&tmp,sizeof(Stu), 1, fp);if(ret == 1){ printf("[tmp]%s, %d\n", tmp.name, tmp.id);}//把文件光标移动到文件开头//fseek(fp, 0, SEEK_SET);rewind(fp);7.4 综合案例1)加密文件读写(使用别人写好的接口) 加密 解密2)配置文件读写(自定义接口)
    八 链表1、数组和链表的区别 数组:一次性分配一块连续的存储区域 优点: 随机访问元素效率高 缺点: 需要分配一块连续的存储区域(很大区域,有可能分配失败) 删除和插入某个元素效率低
    链表:现实生活中的灯笼 优点: 不需要一块连续的存储区域 删除和插入某个元素效率高 缺点: 随机访问元素效率低2、相关概念 节点:链表的每个节点实际上是一个结构体变量,节点,既有 数据域 也有 指针域 typedef struct Node { int id; //数据域 struct Node *next; //指针域 }SLIST;
    尾结点:next指针指向NULL3、结构体套结构体 typedef struct A { int b; }A; / 1)结构体可以嵌套另外一个结构体的任何类型变量 2)结构体嵌套本结构体普通变量(不可以) 本结构体的类型大小无法确定,类型本质:固定大小内存块别名 3)结构体嵌套本结构体指针变量(可以) 链表 指针变量的空间能确定,32位, 4字节, 64位, 8字节
    / typedef struct B { int a; A tmp1; //ok A p1; //ok //struct B tmp2; err struct B next; //32位, 4字节, 64位, 8字节 }B;
    4、链表的分类 1)单向带头链表和不带头链表
    2)双向链表带头链表和不带头链表
    3)双向循环链表带头链表和不带头链表
    5、链表的使用实际上是指针的拓展应用:指向指向谁,就把谁的地址赋值给指针。 typedef struct Stu { int id; //数据域 char name[100]; struct Stu *next; //指针域 }Stu;
    (1)静态链表//初始化三个结构体变量Stu s1 = { 1, "mike", NULL };Stu s2 = { 2, "lily", NULL };Stu s3 = { 3, "lilei", NULL };s1.next = &s2; //s1的next指针指向s2s2.next = &s3;s3.next = NULL; //尾结点Stu *p = &s1; while (p != NULL){ printf("id = %d, name = %s\n", p->id, p->name); //结点往后移动一位 p = p->next; //&s2}(2)动态链表//Stu *p1 = NULL;//p1 = (Stu *)malloc(sizeof(Stu));Stu *p1 = (Stu *)malloc(sizeof(Stu));Stu *p2 = (Stu *)malloc(sizeof(Stu));Stu *p3 = (Stu *)malloc(sizeof(Stu));p1->next = p2;p2->next = p3;p3->next = NULL; //尾节点Stu *tmp = p1;while(tmp != NULL){ printf("id = %d, name = %s\n", tmp->id, tmp->name); //结点往后移动一位 tmp = tmp->next;}6、链表的增、删、改、查操作
    1)单向链表基本操作#define _CRT_SECURE_NO_WARNINGS#include <stdio.h>#include <stdlib.h>#include <string.h>typedef struct Node{ int id; //数据域 struct Node *next; //指针域}Node;//创建头结点//链表的头结点地址由函数值返回。Node *SListCreat(){ Node *head = NULL; //头结点作为标志,不存储有效数据 head = (Node *)malloc(sizeof(Node)); if (head == NULL) { return NULL; } //给head的成员变量赋值 head->id = -1; head->next = NULL; Node *pCur = head; Node *pNew = NULL; int data; while (1) { printf("请输入数据:"); scanf("%d", &data); if (data == -1) //输入-1,退出 { break; } //新结点动态分配空间 pNew = (Node *)malloc(sizeof(Node)); if (pNew == NULL) { //continue; break; } //给pNew成员变量赋值 pNew->id = data; pNew->next = NULL; //链表建立关系 //当前结点的next指向pNew pCur->next = pNew; //pNew下一个结点指向NULL pNew->next = NULL; //把pCur移动到pNew,pCur指向pNew pCur = pNew; } return head;}//链表的遍历int SListPrint(Node * head){ if (head == NULL) { return -1; } //取出第一个有效结点(头结点的next) Node *pCur = head->next; printf("head -> "); while (pCur != NULL) { printf("%d -> ", pCur->id); //当前结点往下移动一位,pCur指向下一个 pCur = pCur->next; } printf("NULL\n"); return 0;}//在值为x的结点前,插入值为y的结点;若值为x的结点不存在,则插在表尾。int SListNodeInsert(Node * head, int x, int y){ if (head == NULL) { return -1; } Node *pPre = head; Node *pCur = head->next; while (pCur != NULL) { if (pCur->id == x) //找到了匹配结点 { break; } //pPre指向pCur位置 pPre = pCur; pCur = pCur->next; //pCur指向下一个结点 } //2种情况 //1. 找匹配的结点,pCur为匹配结点,pPre为pCur上一个结点 //2. 没有找到匹配结点,pCur为空结点,pPre为最后一个结点 //给新结点动态分配空间 Node *pNew = (Node *)malloc(sizeof(Node)); if (pNew == NULL) { return -2; } //给pNew的成员变量赋值 pNew->id = y; pNew->next = NULL; //插入指定位置 pPre->next = pNew; //pPre下一个指向pNew pNew->next = pCur; //pNew下一个指向pCur return 0;}//删除第一个值为x的结点int SListNodeDel(Node *head, int x){ if (head == NULL) { return -1; } Node *pPre = head; Node *pCur = head->next; int flag = 0; //0没有找,1找到 while (pCur != NULL) { if (pCur->id == x) //找到了匹配结点 { //pPre的下一个指向pCur的下一个 pPre->next = pCur->next; free(pCur); pCur = NULL; flag = 1; break; } //pPre指向pCur位置 pPre = pCur; pCur = pCur->next; //pCur指向下一个结点 } if (0 == flag) { printf("没有值为%d的结点\n", x); return -2; } return 0;}//清空链表,释放所有结点int SListNodeDestroy(Node *head){ if (head == NULL) { return -1; } Node * tmp = NULL; int i = 0; while (head != NULL) { //保存head的下一个结点 tmp = head->next; free(head); head = NULL; //head指向tmp head = tmp; i++; } printf("i = %d \n", i); return 0;}//删除值为x的所有结点int SListNodeDelPro(Node *head, int x){ if (head == NULL) { return -1; } Node *pPre = head; Node *pCur = head->next; int flag = 0; //0没有找,1找到 while (pCur != NULL) { if (pCur->id == x) //找到了匹配结点 { //pPre的下一个指向pCur的下一个 pPre->next = pCur->next; free(pCur); pCur = NULL; flag = 1; pCur = pPre->next; //break; continue; //跳出本次循环,重要 } //pPre指向pCur位置 pPre = pCur; pCur = pCur->next; //pCur指向下一个结点 } if (0 == flag) { printf("没有值为%d的结点\n", x); return -2; } return 0;}//链表节点排序int SListNodeSort(Node *head){ if(head == NULL || head->next == NULL) { return -1; } Node *pPre = NULL; Node *pCur = NULL; Node tmp; // pPre->next != NULL,链表倒数第2个结点 for (pPre = head->next; pPre->next != NULL; pPre = pPre->next) { for (pCur = pPre->next; pCur != NULL; pCur = pCur->next) { //注意,排序,除了数据域需要交换,next指针还需要交换 if (pPre->id > pCur->id) //升序 { //只交换数据域 tmp.id = pCur->id; pCur->id = pPre->id; pPre->id = tmp.id; } } } return 0;}//假如原来链表是升序的,升序插入新节点//不能插入节点后再排序,是升序插入新节点xint SListNodeInsertPro(Node *head, int x){ //保证插入前是有序的 int ret = SListNodeSort(head); if (ret != 0) { return ret; } if (head == NULL) { return -1; } Node *pPre = head; Node *pCur = head->next; //1 2 3 5 6, 插入4 //3:pre, 5: cur while (pCur != NULL) { if (pCur->id > x) //找到了匹配结点 { break; } //pPre指向pCur位置 pPre = pCur; pCur = pCur->next; //pCur指向下一个结点 } //给新结点动态分配空间 Node *pNew = (Node *)malloc(sizeof(Node)); if (pNew == NULL) { return -2; } //给pNew的成员变量赋值 pNew->id = x; pNew->next = NULL; //插入指定位置 pPre->next = pNew; //pPre下一个指向pNew pNew->next = pCur; //pNew下一个指向pCur return 0; return 0;}//翻转链表的节点(不是排序,是翻转)//把链表的指向反过来int SListNodeReverse(Node *head){ if (head == NULL || head->next == NULL || head->next->next == NULL) { return -1; } Node *pPre = head->next; Node *pCur = pPre->next; pPre->next = NULL; // head->next->next = NULL; Node *tmp = NULL; while (pCur != NULL) { tmp = pCur->next; pCur->next = pPre; pPre = pCur; pCur = tmp; } //head->next->next = NULL; head->next = pPre; return 0;}int main(void){ Node *head = NULL; head = SListCreat();//创建头结点 SListPrint(head); SListNodeInsert(head, 5, 4); printf("在5的前面插入4后\n"); SListPrint(head); SListNodeDelPro(head, 5); printf("删除所有5结点后\n"); SListPrint(head); SListNodeSort(head); printf("排序后\n"); SListPrint(head); SListNodeInsertPro(head, 6); printf("升序插入6后\n"); SListPrint(head); SListNodeReverse(head); printf("链表翻转后\n"); SListPrint(head); SListNodeDestroy(head); head = NULL; printf("\n"); system("pause"); return 0;}九 函数指针1、指针函数,它是函数,返回指针类型的函数 //指针函数 //()优先级比高,它是函数,返回值是指针类型的函数 //返回指针类型的函数 int fun() { int p = (int )malloc(sizeof(int)); return p; }
    2、函数指针,它是指针,指向函数的指针,(对比数组指针的用法) 一个函数在编译时被分配一个入口地址,这个地址就称为函数的指针,函数名代表函数的入口地址。
    函数指针变量,它也是变量,和int a变量的本质是一样的。int fun(int a){ printf("a ========== %d\n", a); return 0;}//定义函数指针变量有3种方式:(1)先定义函数类型,根据类型定义指针变量(不常用)//有typedef是类型,没有是变量typedef int FUN(int a); //FUN是函数类型,类型模式为: int fun(int);FUN *p1 = NULL; //函数指针变量p1 = fun; //p1 指向 fun 函数fun(5); //传统调用p1(6); //函数指针变量调用方式(2)先定义函数指针类型,根据类型定义指针变量(常用)//()()优先级相同,从左往右看//第一个()代表指针,所以,它是指针//第二个括号代表函数,指向函数的指针typedef int(*PFUN)(int a); //PFUN是函数指针类型PFUN p2 = fun; //p2 指向 funp2(7);(3)直接定义函数指针变量(常用)int(*p3)(int a) = fun;p3(8);int(*p4)(int a);p4 = fun;p4(9);3、函数指针数组,它是数组,每个元素都是函数指针类型 void add() {} void minus() {} void multi() {} void divide() {} void myexit() {}
    //函数指针变量,fun1指向add()函数void(*fun1)() = add;fun1(); //调用add()函数//函数指针数组void(*fun[5])() = { add, minus, multi, divide, myexit };//指针数组char *buf[] = { "add", "min", "mul", "div", "exit" };char cmd[100];int i = 0;while (1){ printf("请输入指令:"); scanf("%s", cmd); for (i = 0; i < 5; i++) { if (strcmp(cmd, buf[i]) == 0) { fun[i](); break; //跳出for()循环,最近的循环 } }}4、回调函数,函数的形参为:函数指针变量 int add(int a, int b) { return a + b; }
    int minus(int a, int b){ return a - b;}//int(*p)(int a, int b), p 为函数指针变量//框架,固定变量//多态,多种形式,调用同一种接口,不一样表现void fun(int x, int y, int(*p)(int a, int b) ){ int a = p(x, y); //回调函数 printf("a = %d\n", a);}typedef int(*Q)(int a, int b); //Q 为函数指针类型void fun2(int x, int y, Q p)//p 为函数指针变量{ int a = p(x, y); //回调函数 printf("a = %d\n", a);}//fun()函数的调用方式fun(1, 2, add);fun2(10, 5, minus);5、函数的递归 递归:函数可以调用函数本身(不要用main()调用main(),不是不行,是没有这么做,往往得不到你想要的结果)
    (1)普通函数调用(栈结构,先进后出,先调用,后结束)void funB(int b){ printf("b = %d\n", b); return;}void funA(int a){ funB(a-1); printf("a = %d\n", a);}调用流程:funA(2) -> funB(1) -> printf(b) (离开funB(),回到funA()函数)-> printf(a)(2)函数递归调用(调用流程和上面是一样,换种模式,都是函数的调用而已)void fun(int a){ if(a == 1) { printf("a == %d\n", a); return; //中断函数很重要 } fun(a-1); printf("a = %d\n", a);}fun(2);(3)递归实现累加 1+2+3+……+100#define _CRT_SECURE_NO_WARNINGS#include <stdio.h>#include <stdlib.h>#include <string.h>int fun(int n){ if (n == 1) { return n; } else { return fun(n - 1) + n; }}int main(void){ int i = 0; int sum = 0; for (i = 1; i <= 100; i++) { sum += i; } printf("sum1 = %d\n", sum); sum = fun(100); printf("\nsum2 = %d\n", sum); printf("\n"); system("pause"); return 0;}(4)函数递归字符串反转十 预处理1、C编译器提供的预处理功能主要有以下四种: 1)文件包含 #include 2)宏定义 #define 3)条件编译 #if #endif .. 4)一些特殊作用的预定义宏
    2、#include< > 与 #include “”的区别“”表示系统先在file1.c所在的当前目录找file1.h,如果找不到,再按系统指定的目录检索。< >表示系统直接按系统指定的目录检索。注意:
    1. #include <>常用于包含库函数的头文件2. #include ""常用于包含自定义的头文件3. 理论上#include可以包含任意格式的文件(.c .h等) ,但我们一般用于头文件的包含。3、宏定义
    #define 宏名 字符串#define PI 3.14#define TEST(a,b) (a)*(b)宏的作用域取消宏定义#undef 宏名4、宏定义函数
    #define MAX2(a,b) (a) > (b) ? (a) : (b)#define MAX3(a,b,c) (a) > (MAX2(b,c)) ? (a) : (MAX2(b,c))5、条件编译 防止头文件被重复包含引用//#pragma once
    //_FUN_H_ 自定义宏,每个头文件的宏都不一样//假如test.h->_TEST_H_#ifndef _FUN_H_#define _FUN_H_ //函数的声明 //宏定义 //结构体#endif //!_FUN_H_6、动态库的封装和使用 socketclient
    7、日志打印 FILE LINE
    8、内存泄漏检查 memwatch
    0 留言 2019-07-30 16:00:39 奖励11点积分
  • C语言学习笔记

    1 愉快的开始hello world1.1 include头文件包含include是要告诉编译器,包含一个头文件;
    在C语言当中,任何库函数调用都需要提前包含头文件;

    <头文件>,代表让C语言编译器去系统目录下寻找相关的头文件;
    “头文件”, 代表让C语言编译器去用户当前目录下寻找相关的头文件;

    如果是使用了一个C语言库函数需要的头文件,那么一定是#include < >;
    如果是使用了一个自定义的h文件,那么一定是#include“ ”。
    1.2 main函数main函数是C语言中的主函数,一个C语言的程序必须有一个主函数,也只能有一个主函数。
    int main() //一个函数在C语言里面,如果只是空(),代表这个函数可以有参数,也可以没有参数int main(void) //如果是(void),就是明确的表达没有任何参数1.3 注释
    // 单行注释,代表注释,就是一个文字说明,没有实质的意义,单行注释是C++语言的注释方法;
    / / 多行注释,多行注释是标准C语言的注释方法。

    1.4 {}括号,程序题和代码块C语言所有的函数的代码都是在{}里包着的
    1.5 声明int a;
    声明一个变量名字叫a,对于C语言,变量的名称是可以自定义的
    1.6 C语言自定义名字的要求可以使用大小写字母,下划线,数字,但第一个字母必须是字母或者下划线(Linux命名法、匈牙利命名法)

    字母区分大小写
    不能用C语言的关键字作为变量名称
    每一行,必须是;结尾

    1.7 printf函数printf是向标准输出设备输出字符串的如果要输出一个字符串:例如:printf(“hello world”);如果要输出一个整数,例如:printf(“%d”,整数);Printf(“\n”);会输出一个回车换行
    1.8 return语句一个函数遇到return语句就终止了,return是C语言的关键字
    1.9 System系统调用System库函数的功能是执行操作系统的命令或者运行指定的程序
    2 C语言中的数据类型2.1 常量常量就是在程序中不可变化的量,常量在定义的时候必须给一个初值。
    2.1.1 #define#define MAX 10 //定义一个宏常量2.1.2 constConst int a=20; //定义一个const常量
    2.2 字符串常量#define STRING “hello world\n”对于#define类型的常量,C语言的习惯是常量名称为大写,但对于普通const常量以及变量,一般为小写结合大写的方式
    2.3 二进制数、位、字节与字我们习惯于十进制的数:10,12等

    一个位只能表示0,或者1两种状态,简称bit
    一个字节为8个二进制,称为8位,简称BYTE,8个比特是一个字节
    一个字位2个字节,简称WORD
    两个字为双字,简称DWORD

    2.4 八进制八进制为以8为基数的数制系统,C语言当中表示八进制0
    2.5 十六进制
    十六进制值16为基数的数制系统,C语言当中用0x表示十六进制
    十进制转化为八进制,用十进制数作为被除数,8作为除数,取商数和余数,直到商数为0的时候,将余数倒过来就是转化后的结果
    十进制转化为十六进制,用十进制数作为被除数,16作为除数,取商数和余数,直到商数为0的时候,将余数倒过来就是转化后的结果

    2.6 原码将最高位作为符号位(0代表正,1代表负),其余各位代表数值本身的绝对值
    2.7 反码
    一个数如果值为正,那么反码和原码相同
    一个数如果为负,那么符号位为1,其他各位与原码相反

    2.8 补码
    正数:原码、反码和补码都相同
    负数:最高位为1,其余各位原码取反,最后对整个数+1
    补码符号位不动,其他位求反,最后整个数+1,得到原码
    用补码进行运算,减法可以通过加法实现

    2.9 sizeof关键字
    Sizeof是C语言关键字,功能是求指定数据类型在内存中的大小,单位:字节,sizeof(int);
    Sizeof与size_f类型

    2.10 int类型2.10.1 int常量,变量int就是32位的一个二进制整数,在内存当中占据4个字节的空间
    2.10.2 printf输出int值
    %d,输出一个有符号的10进制整数
    %u,代表输出一个无符号的10进制整数

    2.10.3 printf输出八进制和十六进制
    %o,代表输出八进制整数
    %x,代表输出16进制整数
    %X,用大写字母方式输出16进制数

    2.10.4 short,long,long long,unsigned int
    Short意思为短整数,在32位系统下是2个字节,16个比特
    Long 意思为长整数,在32位系统下,long都是4个字节的,在64位系统下,windows还是4个字节,unix下成了8个字节。
    int不管是32位系统下,还是64位系统下,不论是windows还是unix都是4个字节的。
    Long long是64位,也就是8个字节大小的整数,对于32位操作系统,CPU寄存器是32位,所以计算long long类型的数据,效率很低

    2.10.5 整数溢出计算一个整数的时候超过整数能够容纳的最大单位后,整数会溢出,溢出的结果是高位舍弃。当一个小的整数赋值给大的整数,符号位不会丢失,会继承。
    2.10.6 大端对齐小端对齐对于ARM,intel这种x86构架的复杂指令CPU,整数在内存中是倒着存放的,低地址放地位,高地址放高位,小端对齐;
    但对于unix服务器的CPU,更多是采用大端对齐的方式存放整数。
    2.11 char类型2.11.1 char常量,变量
    Char c; //定义一个char变量
    ‘a’,char的常量
    char的本质就是一个整数,一个只有1个字节大小的整数

    2.11.2 printf输出char%c意思是输出一个字符,而不是一个整数
    2.11.3 不可打印char转义符\a 警报\b 退格\n 换行\r 回车\t 制表符\\ 斜杠\’ 单引号\” 双引号\?问号2.11.4 char和unsigned char
    Char取值范围为-128到127
    Unsigned char为0-255

    2.12 浮点float,double,long double类型2.12.1 浮点常量,变量
    Float在32位系统下是4个字节,double在32位系统下是8个字节
    小数的效率很低,避免使用,除非明确的要计算一个小数。

    2.12.2 printf输出浮点数
    %f是输出一个double
    %lf输出一个long double

    2.13 类型限定2.13.1 constConst是代表一个不能改变值的常量
    2.13.2 volatile代表变量是一个可能被CPU指令之外的地方改变的,编译器就不会针对这个变量去优化目标代码。
    2.13.3 register变量在CPU寄存器里面,而不是在内存里面,但register是建议型的指令,而不是命令型的指令。
    3 字符串格式化输出和输入3.1 字符串在计算机内部的存储方式字符串是内存中一段连续的char空间,以‘\0’结尾
    “”是C语言表达字符串的方式
    3.2 printf函数,putchar函数Printf格式字符
    字符 对应数据类型 含义d int 接受整数值并将它表示为有符号的十进制整数hd short int 短整数hu unsigned shor int 无符号短整数o unsigned int 无符号8进制整数u unsigned int 无符号10进制整数x/X unsigned int 无符号16进制整数,x对应的是abcdf,X对应的是ABCDEFf float或double 单精度浮点数或双精度浮点数e/E double 科学计数法表示的数,此处“e”的大小写代表在输出是用“e”的大小写c char 字符型,可以把输入的数字按照ASC11码相应转换为对应的字符s/S char */wchar_t * 字符串,输出字符串中的字符直至字符串的空字符(字符串以‘\0’结尾,这个‘\0’即空字符)p void 以16进制形式输出指针% % 输出一个百分号Printf附加格式
    字符 含义| 附加在d,u,x,o前面,表示长整数- 左对齐m(代表一个整数) 数据最小宽度0 将输出的前面补上0,直到占满指定列宽为止(不可以搭 配使用“-”)N(代表一个整数) 宽度至少为n位,不够以空格填充。Putchar是显式一个字符的函数
    long l = 100;Printf(“-6ld”,l);3.3 scanf函数与getchar函数Scanf通过键盘读取用户输入,放入变量中,记得参数一定是变量的地址(&)
    // #define _CRT_SECURE_NO_WARNINGS#pragma warning(disable:4996)Int a=0;Scanf(“%d”,&a);//一定要用&取变量的地址getchar得到用户键盘输入的字符char a = 0;a = getchar();//得到用户键盘的按键printf(“%c”,a);printf(“please input a:”);scanf(“%d”,&a);getchar(); //通过getchar这个函数将之前输入a时候用户按的回车键先收到4 运算符表达式和语句4.1 基本运算符4.1.1 =
    数据对象:泛指数据在内存的存储区域
    左值:表示可以被更改的数据对象
    右值:能赋给左值的量

    4.1.2 +加
    4.1.3 –减
    4.1.4 *乘
    4.1.5 /除
    4.1.6 %取余数
    4.1.7 + =加等于

    a+=5;
    4.1.8 - =减等于
    4.1.9 * =乘等于
    4.1.10 /=除等于
    4.1.11 %=取余等于
    4.1.12 ++自加1

    i++先计算表达式的值,然后再++
    ++i是先++,再计算表达式的值

    4.1.13 —自减1
    4.1.14 逗号运算符int a = 2;int b = 3;int c = 4;int d = 5;int I = (a = b, c + d);逗号表达式先求逗号左边的值,然后求右边的值,整个语句的值是逗号右边的值。
    4.1.15 运算符优先级优先级 运算符 结合性 1 ++(后缀),--(后缀),()(调试函数), 从左到右 {}(语句块),-> 2 ++(前缀),--(前缀),+(前缀),-(前缀), 从右到左 ! (前缀),~(前缀),sizeof,*(取指针值), &(取地址),(type)(类型转化) 3 *,/,% 从左到右 4 +,- 从左到右 5 <<,>> 从左到右 6 < ,>,<=,>= 从左到右 7 = =,!= 从左到右 8 & 从左到右 9 ^ 从左到右 10 | 从左到右 11 && 从左到右 12 || 从左到右 13 ? 从右到左 14 =,*=,%=,+=,-=,<<=,>>=,&=, 从右到左 |=,^= 15 ,(逗号运算符) 从左到右4.2 复合语句{}代码块
    for(i = 0; i < 3; i++) //循环语句,代表复合语句内部的代码要执行3次{ printf(“hello\n”);}4.3 空语句只有一个;号的语句就是空语句,空语句在C语言里面是合法的,并且是在某些场合必用的
    4.4 类型转化double f = (double) 3 / 2; //(double)3意思是将整数3强制转化为double型 ()为强制类型转化运算符double f = 3 /2; //C语言两个整数相除的结果自动转化为一个整数double f = 3.0 / 2;5 条件分支语句5.1 关系运算符在C语言中0代表false,非0代表真
    5.1.1 <小于
    5.1.2 <=小于等于
    5.1.3 >大于
    5.1.4 >=大于等于
    5.1.5 = =等于
    5.1.6 !=不等于
    5.2 关系运算符优先级前四种相同,后两种相同,前四种高于后两种优先级
    5.3 逻辑运算符5.3.1 &&与
    当运算符左右都是真的时候,那么整个表达式的结果为真;只有左右有一个值为假,那么整个表达式的结果为假。
    5.3.2 | |或
    当运算符左右只要有一个值是真的时候,那么整个表达式的结果为真;除非左右两个值都是假,那么整个表达式的结果为假。
    5.3.3 !非
    当值为真的时候,表达式为假;当值为假的时候,表达式为真。
    5.4 if单分支
    if(条件){ //复合语句}当条件是真的时候,复合语句才能被执行,如果条件为假的时候,复合语句不执行
    5.5 if else双分支
    if(条件){ 复合语句1;}else{ 复合语句2;}如果条件为真,那么执行复合语句1,否则执行复合语句2
    5.6 if else if多重if
    if(条件1){ 复合语句1;}else if(条件2){ 复合语句2;}else if(条件3){ 复合语句3;}else { 复合语句4;}当有多个else的时候,else总是和上方最近的那个if语句配对
    5.7 switch与break,default多重选择
    switch(i){case 0: break; //跳出switch的复合语句块……default: //如果所有条件都不满足,那么执行default语句}什么时候用if,什么时候用switch?当条件很复杂,一个条件有&&,||,!存在,那么用if语句如果条件很简单,但分支很多,那么适合用switch
    5.8 条件运算符?一个求绝对值的例子
    int i = -8;int x = (i < 0) ? –i : i;先求?左边的条件,如果条件为真,那么等于:左边的值,否则等于:右边的值一个求最大值的例子
    int c = ( a > b ) ? a : b ;5.9 goto语句与标号无条件跳转goto
    goto end; //无条件的跳转到一个标号去执行……end://标号不建议使用goto语句,goto语句会使你的程序可读性变得很差
    6 循环语句6.1 whilewhile(条件),如果条件为真,循环继续,条件为假,循环结束
    while(1) //是死循环的写法{ 复合语句;}6.2 continue循环遇到continue语句,不再执行continue下面代码,而是直接返回到循环起始语句处继续执行循环
    6.3 break循环遇到break语句,立刻中断循环,循环结束
    6.4 do whiledo{ 复合语句;}while(条件);对于do while来讲,循环的复合语句至少可以被执行一次对于while来讲,有可能复合语句一次执行的机会都没有
    6.5 for
    先执行i=0,对于一个for循环,第一步只执行一次;
    判断i是否小于10,如果i小于10,那么循环继续,否则循环中断
    i++,第一次执行for的时候,不执行i++

    for(int i = 0 ; i < 10 ; i++){ 复合语句;}等同于:
    int = 0;while(i < 10){ i++;}6.6 循环嵌套打印三角形
    * *** ***** ****************int main(){ int i,j; for(i=1;i<7;i++) { for(j=1;j<7-i;j++) { printf(“ ”);}for(j=0;j<(i*2-1);j++){ printf(“*”);}printf(“\n”);}return 0;}7 数组数组的本质就是可以一次定义多个类型相同的变量,同时一个数组中所有的元素在内存中都是顺序存放的。但要记得在C语言中如果定义了如下数组:
    Char s[100] ;//s[0] – s[99],切记没有s[100]这个元素,而且C语言编译器不会帮你检查数组的下标是否有效。Char array[2][3][4] = {};//原则,数组维数越多,代码的可读性就越差,所以要尽可能的用维数少的数组7.1 一维数组定义与使用int array [10]; //定义一个一维数组,名字叫array,一共有10个元素,每个元素都是int类型的array[0] = 20 ;array[1] = 30 ;array[9] = 90 ;//array[10] = 100 ; //错误,没有array[10]这个元素7.2 数组在内存的存储的方式数组在内存中就是一段连续的空间,每个元素的类型是一样的
    7.3 一维数组初始化int array[10] = {1,2,3,4,5,6,7,8,9,10} ;//定义数组的同时为数组的成员初始化值int array[10] = {3,4,5} ;//将数组的前三个元素赋值,其余元素置为0int array[10] = {0} ;//将数组所有的元素都置为0int i;for (i = 0; i < 10; i++){ array[i] = 0 ;//通过循环遍历数组的每个元素,将元素的值置为0 //scanf(“%d”,&array[i]);}求数组中最大元素的值
    int main(){ int array[10] = {32,5,67,98,12,54,8,78,457,10};int max = 0;int i;for (i = 0; i < 10; i++) //想找最大的值,一定要把数组先遍历一遍{ if(max < array[i]) max = array[i]; } printf(“max = %d\n”,max); return 0;}求数组中最小元素的值,和最小值对应的数组下标
    int main(){ int array[10] = {32,5,67,98,12,54,8,78,457,10};int min = array[0];int index = 0; //在没有遍历数组之前,默认数组的第0号元素就是最小的元素int i;for (i = 1; i < 10; i++) //想找最小的值,一定要把数组先遍历一遍{ if(min > array[i]) { index = i; min = array[i]; } } printf(“min = %d index = %d\n”, min , index); return 0;}求数组中所有元素的和
    int main(){ int array[10] = {1,2,3,4,5,6,7,8,9,10};int i;int sum = 0;//存放数组和的变量for (i = 0; i < 10; i++) //想找最大的值,一定要把数组先遍历一遍{ sum += array[i]; } printf(“sum = %d\n”,sum); return 0;}将数组元素逆置
    int main(){ int array[10] = {32,5,67,98,12,54,8,78,457,10};/*int tmp = array[1]; //中间变量实现两个值的互换array[1] = array[0];array[0] = tmp;*/int min = 0; //数组最小下标int max = 9; //数组最大下标while (min < max) //两头往中间堵{ int tmp = array[min]; array[min] = array[max]; array[max] = tmp; min++; max--;} printf(“max = %d min = %d\n”, max , min); return 0;}求100到999之间的水仙花数
    int main(){ int i; for(i = 100 ;i < 1000 ;i++) { Int i1=i%10; // Int i2=i/10%10; // Int i3=i/100; // If((i1*i1*i1+i2*i2*i2+i3*i3*i3) = = i) Printf(“%d\n”,i);}return 0;}求一个int数组中,所有奇数元素的和
    int main(){ int array[10] = {1,2,3,4,5,6,7,8,9,10};int i;int sum = 0;for (i = 0; i < 10; i++) { if((array[i]%2) = = 1) { sum += array[i]; } } printf(“sum = %d\n”,sum); return 0;}求从3到100之间所有素数打印出来 3 5 7 11 13 17 ……
    int main(){int i; //素数是除了1和自己以外,不能被其他整数整除的整数for (i = 3; i < 100; i++) { int j; int ststus = 0; for(j =2 ;j < i ; j++) //判断i是否为素数 { if((i %j) = = 0) { status = 1; break;} } if(status= = 0) //代表这是个素数 { printf(“%d\n”,i);} } return 0;}7.4 二维数组定义与使用int array[2][3];//定义了一个二维数组,有两个array[3]int array[2][3] = { {1,2,3},{4,5,6} };//定义一个二维数组的同时初始化成员7.5 二维数组初始化int a[3][4] = {{1,2,3,4},{5,6,7,8},{9,10,11,12}};int array[2][3] = {0};//将二维数组中每个元素的值都初始化为0 int array[2][3] = { {1,2,3},{4,5,6} }; int i,j; for(j=0;j<3;j++) { int sum=0; for(i=0;i<2;i++) { sum += array[i][j]; } printf("%d\n",sum);//打印列的和 }#include<stdio.h>int main()//冒泡排序{ int array[10] = {34,14,8,54,23,89,56,4,45,22}; int i; int j; for(i = 0;i < 10; i++) { for(j = 1; j < 10 - i; j++) { if(array[j-1] > array[j])//如果前面的元素大于后面的元素,那么就让这两个元素交换位置 { int tmp = array[j]; array[j] = array[j - 1]; array[j-1] = tmp; } } } for(i=0;i<10;i++) { printf("array[%d]=%d\n",i,array[i]); } return 0;}8 字符串与字符数组字符串一定是在内存中以0结尾的一个char数组。
    8.1 字符数组定义char array[100];8.2 字符数组初始化char array[100] = {'a','b','c','d'};char array[100] = ”abcd”;char array[100] = {0};char array[] = “abcd”;8.3 字符数组使用#include<stdio.h>//字符串倒序int main(){ char buf[100] = "hello world"; int len = 0; while (buf[len++]); len--; int i = 0; int j = len -1; while (i < j) { char tmp = buf[i]; buf[i] = buf[j]; buf[j] = tmp; i ++; j --; } printf("%s\n",buf); return 0;}ASCII一个字节存放一个字符。GBK用两个字节存放一个汉字。
    8.4 随机数产生函数rand与srand头文件stdilb.hrand是伪随机数产生器,每次调用rand产生的随机数是一样的。如果调用rand之前先调用srand就出现任意的随机数。只要能保证每次调用srand函数的时候,参数的值是不同的,那么rand函数就一定会产生不同的随机数。
    Srand() //随机数种子发生器Rand() //随机数产生器#include<stdio.h>#include<time.h>#include<stdlib.h>int main(){ int t = (int)time(NULL); srand(t); for(int i=0; i<10; i++) { printf("%d\n",rand()); } return 0;}8.5 用scanf输入字符串“%s”的作用就是输入一个字符串的,scanf是以回车键作为输入完成标志的,但回车键本身并不会作为字符串的一部分如果scanf参数中的数组长度小于用户在键盘输入的长度,那么scanf就会缓冲区溢出,导致程序崩溃。
    8.6 字符串的结束标志scanf将回车、空格都认为是字符串输入结束标志。
    8.7 字符串处理函数8.7.1 getsgets(s);Gets认为回车是输入结束的标志,空格不是输入结束的标志,所以用gets这个函数就可以实现输入带空格的字符串,但是gets和scanf一样存在缓冲区溢出的问题。
    Gets不能用类似“%s”或者“%d”之类的字符转义,只能接受字符串的输入。
    8.7.2 fgets函数gets函数不检查预留缓冲区是否能够容纳用户实际输入的数据。多出来的字符会导致内存溢出,fgets函数改进了这个问题。由于fgets函数是为读取文件设计的,所以读取键盘时没有gets那么方便。
    Char s[100] = {0};fgets(s ,sizeof(s) ,stdin);//第一个参数是char的数组,第二个参数是数组的大小,单位:字节,第三个参数stdin代表标准输入的意思fgets是安全的,不存在缓冲区溢出的问题,调用fgets的时候,只要能保证第二个参数小于等于数组实际的大小,那么就可以避免缓冲区溢出的。
    8.7.3 puts函数Puts函数打印字符串,与printf不同,puts会在最后自动添加一个’\n’
    Char s[] = “hello world”;Puts(s);8.7.4 fputs函数fputs是puts的文本操作版本
    char s[] = “hello world”;fputs(s,stdout);8.7.5 strlen,字符串长度size_t strlen(const char * _str);返回不包含字符串结尾’\n’的字符串长度
    Char s[100] = “hello world”;Int len = strlen(s);Printf(“len = %d\n”,len);8.7.6 strcat,字符串追加size_t strcat(char * _str1,const char * _str2);将参数_str2追加到_str1后尾
    Char s1[100] = “fsfgg”;Strcat(s,s1);Strcat也存在缓冲区溢出的问题
    8.7.7 strncat,字符串有限追加size_t strncat(char * _str1,const char * _str2,size_t len);Strcat(s,s1,3); //合并的时候可以限制追加多少个字符8.7.8 strcmp,字符串比较int strcmp(const char * _str1,const char * _str2);比较两个字符串是否相等,相等返回0,不相等返回非0
    If(strcmp(s1,s2)) = = 0){ Printf(“相同\n”);}8.7.9 strncmp,字符串有限比较If(strncmp(s1,s2,5) = = 0) //strncmp的意思是只比较指定数量的字符8.7.10 strcpy字符串拷贝Char *strcpy(char * _str1,const char * _str2);将参数_str2拷贝到参数_str1中
    Strcpy(s1,s2);8.7.11 strncpy字符串有限拷贝Strncpy(s1,s2,3);8.7.12 sprintf格式化字符串和printf函数功能类似,printf函数将格式化结果输出到屏幕,sprintf将格式化结果输出到字符串。
    int i = 200;char s[100] = {0};sprintf(s, “I = %d”,i);8.7.13 Sscanf函数Sscanf类似于scanf函数,scanf从键盘读取用户输入,scanf从指定格式化字符串读取输入。
    8.7.14 strchr查找字符Char * strchr(char * _Str, int _Ch);在参数_str中查找参数_ch指定字符,找到返回字符_ch在_str中所在位置,没有找到返回NULL;
    8.7.15 strstr查找子串Char * strstr(char * _Str,const char * _SubStr);在参数_str中查找参数_SubStr指定子串,找到返回子串在_Str中所在位置,没有找到返回NULL;
    8.7.16 strtok分割字符串字符在第一次调用时strtok()必需给予参数s字符串,往后的调用则将参数s设置成NULL每次调用成功则返回指向被分割出片段的指针。
    如果strtok没有找到指定的分割符号,那么返回NULL
    Char buf [] = “abcd@efg@h”;Char *p = strtok(buf,”@”);While(p){ Printf(“%s\n”,p); P = strtok(NULL,”@”);}8.7.17 atoi转化为intint i1 = atoi(a); //将字符串转化为一个整数需要包含头文件stdlib.h
    8.7.18 atof转化为float8.7.19 atol转化为long#include <string.h> //计算字符串int calc_string(const char *s){ char buf1[100] = {0}; char oper1 = 0; char buf2[100] = {0}; int len = strlen(s);//得到字符串的长度 int i; int start; for(i=0;i<len;i++) { if(s[i] == '+'|| s[i] == '-' || s[i] == '*' || s[i] == '/') { strncpy(buf1, s, i); oper1 = s[i]; break; } } start = i + 1; for(;i<len;i++) { if(s[i] == '=') { strncpy(buf2,&s[start],i-start); } } printf("buf1 = %s,oper1 = %c,buf2 = %s\n",buf1,oper1,buf2); switch(oper1) { case '+': return atoi(buf1) + atoi(buf2); case '-': return atoi(buf1) - atoi(buf2); case '*': return atoi(buf1) * atoi(buf2); case '/': { int a = atoi(buf2); if(a) return atoi(buf1) / atoi(buf2); else return 0; } }}int main(){ const char *s = "32 + 56 ="; printf("%d\n",calc_string(s)); return 0;}9 函数9.1 函数的原型和调用在使用函数前必须定义或者声明函数
    9.2 函数的形参与实参在调用函数的时候,函数大多数都有参数,主调函数和被调用函数之间需要传递数据。在定义函数时函数名后面括弧中的变量名称为“形式参数”,简称形参。在调用函数时,函数名后面括号中的变量或表达式称为“实际参数”,简称实参。

    形参在未出现函数调用时,他们并不占用内存单元,只有在发生函数调用的时候形参才被分配内存,函数调用完成后,形参所占的内存被释放;
    实参可以是变量,常量或者表达式;
    在定义函数时,一定要指定形参的数据类型;
    形参与实参的数据类型一定要可兼容;
    在C语言中,实参与形参的数据传递是“值传递”,即单向传递,只由实参传递给形参,而不能由形参传递给实参。

    如果函数的参数是个数组,那么是可以通过形参修改实参的值的
    9.3 函数的返回类型与返回值
    函数的返回值通过函数中的return获得,如果函数的返回值为void可以不需要return语句;
    函数return语句中的返回值数据类型应该与函数定义时相同;
    如果函数中没有return语句,那么函数将返回一个不确定的值。

    如果C语言一个函数没有明确的标明函数的返回类型,那么函数的返回类型就是int;如果一个函数没有返回值,那么函数的返回类型是void;
    9.4 main函数与exit函数与函数的return语句exit(0); //在子函数中调用exit同样代表程序终止,但在子函数中调用return只是子函数终止,程序正常执行。exit是C语言的库函数,调用exit的结果就是程序终止,在main函数中调用exit与调用return是一样的;
    main函数return代表程序终止。
    9.5 多个源代码文件程序的编译9.5.1 头文件的使用如果把main函数放在第一个文件中,而把自定义函数放在第二个文件中,那么就需要在第一个文件中声明函数原型。如果把函数原型包含在一个头文件里,那么就不必每次使用函数的时候都声明其原型了。把函数声明放入头文件是很好的习惯。
    9.5.2 #include与#define的意义#include就是简单的文件内容替换#define就是简单的文件替换而已9.5.3 #ifndef 与#endif在头文件.h中,
    #ifndef _宏名_#define _宏名_//具体宏的名字是自定义的//函数的声明#endif作用:防止多次include的同一个头文件的时候,重复预编译头文件内容防止头文件被重复包含#ifndef的意思就是条件预编译,如果#ifndef后面的条件成立,那么就预编译从#ifndef开始到#endif之间的代码,否则不会去预编译这段代码。在#ifndef中的宏,一定要大写和下划线,必要的时候加数字,目的是为了避免和其他头文件中的宏名字冲突。#ifdef,#ifndef叫条件编译语句;#ifdef 宏,如果宏被定义了,那么编译语句;#ifndef 宏,如果宏被定义了,那么就不编译语句。9.6 函数的递归函数可以调用自己,这就叫函数的递归。
    #include <stdio.h>void test(int n){ if(n > 0) { n --; printf("先序n = %d\n",n);//先序递归,如果是先序递归,那么代码是顺序执行的 test(n);//函数自己调用自己,就叫函数的递归 printf("后序n = %d\n",n);//后序递归,如果是后序递归,那么代码是逆序执行的 }}int main(){ int i = 3; test(i); return 0;}9.6.1 递归的过程分析案例:将十进制转换为二进制
    #include <stdio.h>void test(int n){ int i = n % 2; printf("%d\n",i); if(n > 0) { test(n / 2); }}int main(){ int i = 11; test(i); return 0;}斐波那契数列例子:斐波那契数列指的是这样一个数列0,1,1,2,3,5,8,13,21,34,55,89,144,…第0项是0,第1项是第一个1;这个数列从第2项开始,每一项都等于前两项之和。
    int fib(int n){ if (n == 0) return 0; if (n == 1) return 1; else { return fib(n - 1) + fib(n - 2); }}9.6.2 递归的优点递归给某些编程问题提供了最简单的方法。
    9.6.3 递归的缺点一个有缺陷的递归会很快耗尽计算机的资源,递归的程序难以理解和维护。
    10 指针10.1 指针10.1.1 指针的概念指针变量也是一个变量;指针存放的内容是一个地址,该地址指向一块内存空间
    10.1.2 指针变量的定义可以定义一个指向一个变量的指针变量
    Int *p; //表示定义一个指针变量*p; //代表指针所指内存的实际数据切记,指针变量只能存放地址,不能将一个int型变量直接赋值给一个指针。Int *p = 100;Int *p = &a; //得到变量a的地址,将这个地址赋值给变量pInt *p1;//定义一个变量,名字叫p1,它可以指向一个int的地址P1=&b;//指针变量的值一般不能直接赋值一个整数,而是通过取变量地址的方式赋值10.1.3 &取地址运算符&可以取得一个变量在内存当中的地址
    10.1.4 无类型指针定义一个指针变量,但不指定它指向具体哪种数据类型。可以通过强制转化将void 转化为其他类型指针,也可以用(void )将其他类型指针强制转化为void类型指针。
    Void *p;10.1.5 NULLNULL在C语言中的定义为(void *)0;当一个指针不指向任何一个有效内存地址的时候,我们应该把指针设置为NULL。
    10.1.6 空指针与野指针指向NULL的指针叫空指针,没有具体指向任何变量地址的指针叫野指针。空指针是合法的,但野指针是危险的,是导致程序崩溃的主要原因。
    10.1.7 指针的兼容性指针之间赋值比普通数据类型赋值检查更为严格,例如:不可以把一个double *赋值给int *;原则上一定是相同类型的指针指向相同类型的变量地址,不能用一种类型的指针指向另一种类型的变量地址。
    10.1.8 指向常量的指针与指针常量Const char * p; //定义一个指向常量的指针Char *const p;//定义一个指针常量,一旦初始化之后其内容不可改变。10.1.9 指针与数组的关系一个变量有地址,一个数组包含若干个元素,每个元素在内存中都有地址。
    Int a[10];Int *p = a;比较p和&a[0]的地址是否相同。
    10.1.10 指针运算指针运算不是简单的整数加减法,而是指针指向的数据类型在内存中占用字节数作为倍数的运算。
    Char *p;P++; //移动了sizeof(char)这么多的字节数int *p1;P1++; //移动了sizeof(int)这么多的字节数赋值:int *p = &a;求值:int i = *p;取指针地址int **pp = &p;将一个整数加(减)给指针:p+3;p-3;增加(减少)指针值 p++,p--求差值,p1-p2,通常用于同一个数组内求两个元素之间的距离比较p1= =p2,通常用来 比较两个指针是否指向同一个位置10.1.11 通过指针使用数组元素P+1代表&a[1],也可以直接使用p[1]表示a[5]P +5代表&a[5];P++
    寻找数组第二大元素第一步:假设数组中前2个元素就是最大的和第二大的MaxSmax第二步:从数组的第2号元素开始遍历数组当有元素大于max的时候,Smax = maxMax= 最大的那个元素,如果当前元素小于max,并且大于smax,那么就让smax等于当前那个元素
    int smax(int *s)//求数组中第二大元素{ int max = 0; int s_max = 0; int i; if (*s > *(s +1)) { max = *s; s_max = *(s + 1); } else { max = *(s + 1); s_max = *s; }//将max等于s[0]和s[1]中大的那个元素的值 for(i = 2;i < 10;i++)//从第3个元素开始遍历数组 { if(max < *(s + i))//如果遇到大于max的元素,那么让s_max等于max,让max等于这个元素 { s_max = max; max = *(s + i); } else if(max > *(s + i) && *(s + i) > s_max)//如果这个元素是介于max和s_max之间,那么就让这个元素等于s_max { s_max = *(s + i); } } return s_max;//返回s_max的值}int main(){ int buf[10] = {34,21,56,4,87,90,15,65,72,48}; printf("%d\n",smax(buf)); return 0;}#include<string.h>//通过指针将字符串逆置int main(){ char str[100]="you"; char *str_start = &str[0]; char *str_end = &str[strlen(str) - 1]; while(str_start < str_end) { char *tmp = * str_start; * str_start = * str_end; * str_end = tmp; str_start ++; str_end --; } printf("%s\n",str); return 0;}对于VS的汉字是GBK编码,一个汉字2个字节;对于QT汉字是UTF8编码,一个汉字是3个字节。
    #include<string.h>//通过指针将汉字字符串逆置int main(){ char str[100]="我爱你"; short *str_start = &str[0]; short *str_end = &str[strlen(str) - 2]; while(str_start < str_end) { short *tmp = * str_start; * str_start = * str_end; * str_end = tmp; str_start ++; str_end --; } printf("%s\n",str); return 0;}10.1.12 指针数组int *a[10];//定义了一个指针数组,一共10个成员,其中每个成员都是int *类型10.1.13 指向指针的指针(二级指针)指针就是一个变量,既然是变量就也存在内存地址,所以可以定义一个指向指针的指针。通过二级指针修改内存的值;
    Int I = 10;Int *p1 = &I;Int **p2 = &p1;Printf(“%d\n”,**p2);以此类推可以定义3级甚至多级指针。C语言允许定义多级指针,但是指针级数过多会增加代码的复杂性,考试的时候可能会考多级指针,但是实际编程的时候最多用到3级指针,但是3级指针也不常用,一级和二级指针是大量使用的。
    10.1.14 指向二维数组的指针Int buf[3][5] 二维数组名称,buf代表数组首地址Int (*a)[5] 定义一个指向int[5]类型的指针变量aa[0],*(a+0),*a 0行,0列元素地址a+1 第1行首地址a[1],*(a+1) 第1行,0列元素地址a[1]+2,*(a+1)+2,&a[1][2] 第1行,2列元素地址*(a[1]+2),*(*(a+1)+2),a[1][2] 第1行,2列元素的值//二维数组的指针计算二维数组行列的平均值int main(){ int buf[3][5] = {{2,4,3,5,1},{7,2,6,8,1},{7,3,9,0,2}}; int i; int j; int sum; for(i = 0;i < 3;i ++) { sum = 0; for(j = 0;j < 5;j ++) { sum += (*(*(buf + i) + j)); //sum += buf[i][j]; } printf("%d\n",sum / 5); } for(i = 0;i < 5;i ++) { sum = 0; for(j = 0;j < 3;j ++) { sum += (*(*(buf + j) + i)); //sum += buf[j][i]; } printf("%d\n",sum / 3); } return 0;}10.1.15 指针变量作为函数的参数函数的参数可以是指针类型 *,它的作用是将一个变量的地址传送给另一个函数。通过函数的指针参数可以间接的实现形参修改实参的值。
    10.1.16 一维数组名作为函数参数当数组名作为函数参数时,C语言将数组名解释为指针
    int func(int array[10]);//数组名代表数组的首地址10.1.17 二维数组名作为函数参数二维数组做函数参数时可以不指定第一个下标。
    int func(int array[][10]);将二维数组作为函数的参数用例不是特别多见。
    10.1.18 const关键字保护数组内容如果讲一个数组作为函数的形参传递,那么数组内容可以在被调用函数内部修改,有时候不希望这样的事情发生,所以要对形参采用const参数。
    func(const int array[]);10.1.19 指针作为函数的返回值return NULL;10.1.20 指向函数的指针指针可以指向变量、数组,也可以指向一个函数。一个函数在编译的时候会分配一个入口地址,这个入口地址就是函数的指针,函数名称就代表函数的入口地址。函数指针的定义方式:
    int (*p)(int);//定义了一个指向int func(int n)类型函数地址的指针。
    定义函数指针变量的形式为:函数返回类型(*指针变量名称)(参数列表)
    函数可以通过函数指针调用

    int(* p)()代表指向一个函数,但不是固定哪一个函数Void *p(int ,char *);//声明了一个函数,函数的名字叫p,函数的返回值是void *,函数的参数是int和char *Void (*p)(int ,char *);//定义了一个指向参数为int和char *,返回值为void的函数指针Int *(*p)(int *);//定义一个参数为int *,返回值为int *的指向函数的指针Int *p(int *);//声明了一个函数,返回值是int *,参数是int *Int (*p)(int , int );//定义了一个指向函数的指针,可以指向两个参数,都是int,返回值也是int类型在回调函数和运行期动态绑定的时候大量的用到了指向函数的指针。
    10.1.21 把指向函数的指针作为函数的参数将函数指针作为另一个函数的参数称为回调函数。
    int max(int a,int b){ if(a > b) return a; else return b;}int add(int a,int b){ return a + b;}int func(int (*p)(int,int),int a,int b)//第一个参数是指向函数的指针{ return p(a,b);//通过指向函数的指针调用一个函数}int main(){ int i = func(add,6,9);//add函数在这里就叫回调函数 printf("i = %d\n",i); return 0;}10.1.22 memset,memcpy,memmove函数这三个函数分别实现内存设置,内存拷贝,内存移动使用memcpy的时候,一定要确保内存没有重叠区域。
    memset(buf, 0, sizeof(buf));//第一个参数是要设置的内存地址,第二个参数是要设置的值,第三个参数是内存大小,单位:字节memcpy(buf2, buf1, sizeof(buf1));//将buf1的内存内容全部拷贝到buf2,拷贝大小为第三个参数:字节memmove(buf2, buf1, sizeof(buf1));//并没有改变原始内存的值
    10.1.23 指针小结定义 说明Int I 定义整形变量Int *p 定义一个指向int的指针变量Int a[10] 定义一个int数组Int *p[10] 定义一个指针数组,其中每个数组元素指向一个int型变量的地址Int (*p)[10] 定义一个数组指针,指向int[10]类型的指针变量Int func() 定义一个函数,返回值为int型Int *func() 定义一个函数,返回值为int *型Int (*p)() 定义一个指向函数的指针,函数的原型为无参数,返回值为intInt **p 定义一个指向int的指针的指针,二级指针11 字符指针与字符串11.1 指针和字符串在C语言中,大多数字符串操作其实就是指针操作。
    Char s[] = “hello world”;Char *p = s;P[0] = ‘a’;11.2 通过指针访问字符串数组char buf[100] = "hello world";char *p = buf;//*(p + 5) = 'a';//p[5] = 'b';p += 5;*p = 'c';p[3] = ' ';printf("buf = %s\n",buf);11.3 函数的参数为char *void print_array(int *p,int n)//如果参数是一个int数组,那么就必须传递第二个参数用来标示数组的长度{ int i; for(i = 0; i <n; i++) { printf("p[%d] = %d\n", i, p[i]); }}void print_str(char *s)//如果参数是个字符串,那么就不需要包含第二个参数//因为字符串是明确的以‘\0’结尾的,所以在函数内部是有条件来作为循环终止依据的{ int i = 0; while(s[i]) { printf("%c",s[i++]); }}11.4 指针数组作为main函数的形参Int main(int argc, char *argv[]);main函数是操作系统调用的,所以main函数的参数也是操作系统在调用时候自动填写的argc代表命令行参数的数量argv代表命令行的具体参数,是char *类型的
    12 内存管理12.1 作用域一个C语言变量的作用域可以是代码块作用域,函数作用域或者文件作用域。代码块是{}之间的一段代码。出现在{}之外的变量,就是全局变量。
    12.1.1 auto自动变量一般情况下代码块内部定义的变量都是自动变量。当然也可以显示的使用aotu关键字
    12.1.2 register寄存器变量通常变量在内存当中,如果能把变量放到CPU的寄存器里面,代码执行效率会更高register int i;//建议,如果有寄存器空闲,那么这个变量就放到寄存器里面使用对于一个register变量,是不能取地址操作的
    12.1.3 代码块作用域的静态变量静态变量是指内存位置在程序执行期间一直不改变的变量,一个代码块内部的静态变量只能被这个代码块内部访问。static int i = 0;//静态变量,只初始化一次,而且程序运行期间,静态变量一直存在
    12.1.4 代码块作用域外的静态变量代码块之外的静态变量在程序执行期间一直存在,但只能被定义这个变量的文件访问Static int a=0;//一旦全局变量定义static,意思是这个变量只是在定义这个变量的文件内部全局有效
    12.1.5 全局变量全局变量的存储方式和静态变量相同,但可以被多个文件访问
    12.1.6 外部变量与extern关键字extern int i;
    12.1.7 全局函数和静态函数在C语言中函数默认都是全局的,使用关键字static可以将函数声明为静态
    12.2 内存四区12.2.1 代码区代码区code,程序被操作系统加载到内存的时候,所有的可执行代码都加载到代码区,也叫代码段,这块内存是不可以运行期间修改的。
    12.2.2 静态区所有的全局变量以及程序中的静态变量都存储到静态区,比较如下两段代码的区别:
    int a=0; int a=0;int main() static int b=0;{ int main() static int b=0; {printf(“%p,%p\n”,&a,&b); printf(“%p,%p\n”,&a,&b);return 0; retrun 0;} }int *getb() //合法的{ static int a=0; return &a;}12.2.3 栈区栈stack是一种先进后出的内存结构,所有的自动变量,函数的形参都是由编译器自动放出栈中,当一个自动变量超出其作用域时,自动从栈中弹出。
    对于自动变量,什么时候入栈,什么时候出栈,是不需要程序控制的,由C语言编译器实现栈不会很大,一般都是以K为单位的
    int *geta()//错误,不能将一个栈变量的地址通过函数的返回值返回{ int a = 0; return &a;}12.2.4 栈溢出当栈空间已满,但还往栈内存压变量,这个就叫栈溢出。对于一个32位操作系统,最大管理4G内存,其中1G是给操作系统自己用的,剩下的3G都是给用户程序的,一个用户程序理论上可以使用3G的内存空间。
    12.2.5 堆区堆heap和栈一样,也是一种在程序运行过程中可以随时修改的内存区域,但没有栈那样先进后出的顺序。堆是一个大容器,它的容量要远远大于栈,但是在C语言中,堆内存空间的申请和释放需要手动通过代码来完成。
    int *geta()//可以通过函数的返回值返回一个堆地址,但记得一定要free{ int *p=malloc(sizeof(int ));//申请了一个堆空间 return p;}Int main(){ Int *getp=geta(); *getp=100; free(getp);}12.3 堆的分配和释放操作系统在管理内存的时候,最小单位不是字节,而是内存页。
    12.3.1 mallocvoid * malloc(size_t _Size);int *p=(int *)malloc(sizeof(int)*10);//在堆中间申请内存,在堆中申请了一个10个int这么大的空间malloc函数在堆中分配参数_Size指定大小的内存,单位:字节,函数返回void *指针。Void getheap(int *p){ P=malloc(sizeof(int) * 10);}//getheap执行完以后,p就消失了,导致它指向的具体堆空间的地址编号也随之消失了Void getheap1(int **p){ *p=malloc(sizeof(int) * 10);}Int main(){ Int *p=NULL; Printf(“p=%p\n”,&p); //getheap(p);//实参没有任何改变 getheap1(&p);//得到了堆内存的地址}12.3.2 freeVoid free(void *p);free(p);//释放通过malloc分配的堆内存free负责在堆中释放malloc分配的内存。参数p为malloc返回的堆中的内存地址。Int *array = malloc(sizeof(int) * i);//在堆当中动态创建一个int数组free(array);12.3.3 callocVoid * calloc(size_t _Count, size_t _Size);Calloc与malloc类似,负责在堆中分配内存。
    第一个参数是所需内存单元数量,第二个参数是每个内存单元的大小(单位:字节),calloc自动将分配的内存置0
    Int *p=(int *)calloc(100,sizeof(int));//分配100个int12.3.4 realloc重新分配用malloc或者calloc函数在堆中分配内存空间的大小。
    Void * realloc(void *p, size_t _NewSize);第一个参数p为之前用malloc或者calloc分配的内存地址,_NewSize为重新分配内存的大小,单位:字节。成功返回新分配的堆内存地址,失败返回NULL;如果参数p等于NULL,那么realloc与malloc功能一致。
    Char *p = malloc(10);//分配空间,但原有数据没做清洁Char *p1 = calloc(10, sizeof(char));//分配空间以后,自动做清洁Char *p2 =realloc(p1,20);//在原有内存基础之上,在堆中间增加连续的内存//如果原有内存没有连续空间可扩展,那么会新分配一个空间,将原有内存copy到新空间,然后释放原有内存。//realloc和malloc,只分配内存,但不打扫Char *p2 = realloc(NULL,5);//等于malloc(5)13 结构体,联合体,枚举与typedef13.1 结构体13.1.1 定义结构体struct和初始化Struct man{ Char name[100]; Int age;};Struct man m = {“tom”,12};Struct man m = {.name = “tom”, .age = 12};#include <string.h>#pragma warning(disable:4996)struct student{ char name[100]; int age; int sex;};//说明了一个结构体的数据成员类型int main(){ struct student st;//定义了一个结构体的变量,名字叫st; st.age = 20; st.sex = 0; strcpy(st.name,"刘德华"); printf("name = %s\n",st.name); printf("age = %d\n",st.age); if(st.sex == 0) { printf("男"); } else { printf("女"); } return 0;}13.1.2 访问结构体成员.操作符
    13.1.3 结构体的内存对齐模式编译器在编译一个结构的时候采用内存对齐模式。
    Struct man{ Char a; Int b; Shor c; Char d; Long long e;};13.1.4 指定结构体元素的位字段定义一个结构体的时候可以指定具体元素的位长;
    Struct test{ char a : 2;//指定元素为2位长,不是2个字节长};13.1.5 结构数组Struct man m[10] = {{“tom”,12},{“marry”,10},{“jack”,9}};Int I;for(i=0; i<5; i++){ Printf(“姓名=%s,年龄=%d\n”,m[i].name,m[i].age);}#include <stdio.h>#include <string.h>#pragma warning(disable:4996)//结构体数组排序struct student{ char name[100]; int age; int score; char classes[100];};void swap(struct student *a,struct student *b){ struct student tmp = *a; *a = *b; *b = tmp;}int main(){ struct student st[5] = {{"chen",12,78,"A"},{"li",10,90,"B"},{"wang",13,59,"C"},{"fei",12,91,"D"},{"bai",9,59,"E"}}; int i; int j; for(i = 0; i < 5; i++) { for(j = 1; j < 5-i; j++) { if(st[j].age < st[j - 1].age) { swap(&st[j],&st[j - 1]); } else if(st[j].age == st[j - 1].age) { if(st[j].score <st[j-1].score) { swap(&st[j],&st[j - 1]); } } } } for(i = 0; i < 5; i++) { printf("姓名=%s,年龄=%d,成绩=%d,班级=%s\n",st[i].name,st[i].age,st[i].score,st[i].classes); } return 0;}13.1.6 嵌套结构一个结构的成员还可以是另一个结构类型
    Struct names{ Char a; Int b;};Struct man{ Struct names name; Int c;};Struct man m = {{“wang”,10},20};13.1.7 结构体的赋值Struct name m = b;
    结构体赋值,其实就是结构体之间内存的拷贝
    13.1.8 指向结构体的指针—>操作符Struct A a;Struct A *p = &a;p->a = 10;13.1.9 指向结构体数组的指针13.1.10 结构中的数组成员和指针成员一个结构中可以有数组成员,也可以有指针成员,如果是指针成员结构体成员在初始化和赋值的时候就需要提前为指针成员分配内存。
    Struct man{ Char name[100]; Int age;};Struct man{ Char *name; Int age;};13.1.11 在堆中创建的结构体如果结构体有指针类型成员,同时结构体在堆中创建,那么释放堆中的结构体之前需要提前释放结构体中的指针成员指向的内存。
    Struct man{ Char *name; Int age;};Struct man *s = malloc(sizeof(struct man) * 2);S[0].name = malloc(10 * sizeof(char));S[1].name = malloc(10 * sizeof(char));13.1.12 将结构作为函数参数将结构作为函数参数将结构指针作为函数参数
    Struct student{ Char name[10]; Int age;};Void print_student (struct student s)//一般来讲,不要把结构变量作为函数的参数传递,因为效率比较低,一般用指针来代替//void print_student(const struct student *s){ Printf(“name = %s, age = %d\n”,s.name,s.age);}Int main(){ Struct student st = {“tom”,20}; Print_student(st);}13.1.13 结构,还是指向结构的指针在定义一个和结构有关的函数,到底是使用结构,还是结构的指针?指针作为参数,只需要传递一个地址,所以代码效率高。
    Void set_student (struct student *s, const char *name, int age){ Strcpy(s->name, name); s->age = age;}Int main(){ Set_student(&st, “maik”,100); Print_student(st);}结论:当一个结构作为函数的参数时候,尽量使用指针,而不是使用结构变量,这样代码效率很高。
    13.2 联合体联合union是一个能同一个存储空间存储不同类型数据的类型。联合体所占的内存长度等于其最长成员的长度,也有叫做共用体。联合体虽然可以有多个成员,但同一时间只能存放其中一种。
    union A{ Int a; Char b; Char *c; //注意};Int main(){ Union A a; a.a = 10;a.c = malloc(100);//c指向了一个堆的地址 free(c);//如果联合体中有指针成员,那么一定要使用完这个指针,并且free指针之后才能使用其他成员 a.b = 20;}13.3 枚举类型13.3.1 枚举定义可以使用枚举声明代表整数常量的符号名称,关键字enum创建一个新的枚举类型。实际上,enum常量是int类型的。枚举是常量,值是不能修改的。
    enum spectrum{ red,yellow,green,blue,white,black};enum spectrum color;color = black;if(color != red)13.3.2 默认值默认时,枚举列表中的常量被指定为0,1,2等
    enum spectrum{ red,yellow,green,blue,white,black};printf(“%d,%d\n”,red,black);指定值可以指定枚举中具体元素的值
    enum spectrum{ red=1,yellow=2,green=3,blue,white,black};13.4 typedeftypedef是一种高级数据特性,它能使某一类型创建自己的名字。
    typedef unsigned char UBYTE;typedef char BYTE;
    与#define不同,typedef仅限于数据类型,而不是能是表达式或具体的值
    typedef是编译器处理的,而不是预编译指令;
    typedef比#define更灵活

    直接看typedef好像没什么用处,使用BYTE定义一个unsigned char。使用typedef可以增加程序的可移植性。
    typedef struct{ Int a;}A2;A2 a;13.5 通过typedef定义函数指针typedef const char *(*SUBSTR)(const char *,const char *);const char *getsubstr(const char *src, const char *str){ return strstr(src,str);}Const char *(*p[3])(const char *,const char *);14 文件操作14.1 fopenChar s[1024] = {0};FILE *p = fopen(“D:\\temp\\a.txt”,”w”);//用写的方式打开一个文件fputs(“hello world”,p);//向文件写入一个字符串//feof(p);//如果已经到了文件结尾,函数返回真While(!feof(p))//如果没有到文件结尾,那么就一直循环{ memset(s,0,sizeof(s));//fgets(s,sizeof(s),p);//第一个参数是一个内存地址,第二个参数是这块内存的大小,第三个参数是fopen返回的文件指针printf(“%s”,s);}fclose(p);//关闭这个文件r以只读方式打开文件,该文件必须存在r+ 以可读写方式打开文件,该文件必须存在rb+ 读写打开一个二进制文件,允许读写数据,文件必须存在rw+ 读写打开一个文本文件,允许读和写w 打开只写文件,若文件存在则文件长度清0,即该文件内容会消失。若文件不存在则建立该文件;w+ 打开可读写文件,若文件存在则文件长度清0,即该文件内容会消失。若文件不存在则建立该文件;a 以附加的方式打开只写文件。若文件不存在,则会建立该文件,如果文件存在,写入的数据会被加到文件尾,即文件原先的内容会被保留。(EOF符保留)a+ 以附加的方式打开可读写文件。若文件不存在,则会建立该文件,如果文件存在,写入的数据会被加到文件尾,即文件原先的内容会被保留。(原来的EOF符不保留)。
    Void code(char *s){ While(*s)//遍历一个字符串 { (*s)++; s++;}}Int main()//文件加密{ Char s[1024] = {0};FILE *p = fopen(“D:\\temp\\a.txt”,”r”);//用读的方式打开一个文件FILE *p1 = fopen(“D:\\temp\\a.txt”,”w”);//用写的方式打开一个文件//feof(p);//如果已经到了文件结尾,函数返回真While(!feof(p))//如果没有到文件结尾,那么就一直循环{ memset(s,0,sizeof(s));fgets(s,sizeof(s),p);//第一个参数是一个内存地址,第二个参数是这块内存的大小,第三个参数是fopen返回的文件指针code(s);//文件加密fputs(s,p1);}fclose(p);//关闭这个文件fclose(p1);}14.2 二进制和文本模式的区别
    在windows系统中,文本模式下,文件以“\r\n”代表换行。若以文本模式打开文件,并用fputs等函数写入换行符”\n”时,函数会自动在”\n”前面加上“\r”。即实际写入文本的是”\r\n”。
    在类Unix/Linux系统中文版模式下,文件以”\n”代表换行。所以Linux系统中在文本模式和二进制模式下并无区别。

    14.3 fclosefclose关闭fopen打开的文件
    14.4 getc和putc函数Int main() int main(){ { FILE *fp=fopen(“a.txt”,”r”); FTLE *fp=fopen(“a.txt”,”w”); Char c; const char *s=”hello world”; While((c=getc(fp))!=EOF) int I; { for(i=0;i<strlen(s);i++) Printf(“%c”,c); {} putc(s[i],fp);fclose(fp); }return 0; fclose(fp);} return 0; }14.5 EOF与feof函数文件结尾程序怎么才能知道是否已经到达文件结尾了呢?EOF代表文件结尾如果已经是文件尾,feof函数返回true。
    While((c=getc(p))!=EOF)//EOF代表文件最后的一个结束标识{ //c=getc(p);//一次只读取一个字符 Printf(“%c”,c);}##14.6 fprintf,fscanf,fgets,fputs函数这些函数都是通过FILE *来对文件进行读写。都是针对文本文件的行读写函数fprintf(p,”%s”,buf);//和printf功能一样,fprintf将输入的内容输入到文件里面fscanf(p,”%s”,buf); //fscanf与scanf用法基本一致,fscanf是从一个文件读取输入,scanf是从键盘读取输入#include <stdio.h>#include <stdlib.h>#include <string.h>//计算文本中的字符串int calc_string(const char *s){ char buf1[100] = {0}; char oper1 = 0; char buf2[100] = {0}; int len = strlen(s);//得到字符串的长度 int i; int start; for(i=0;i<len;i++) { if(s[i] == '+'|| s[i] == '-' || s[i] == '*' || s[i] == '/') { strncpy(buf1, s, i); oper1 = s[i]; break; } } start = i + 1; for(;i<len;i++) { if(s[i] == '=') { strncpy(buf2,&s[start],i-start); } } printf("buf1 = %s,oper1 = %c,buf2 = %s\n",buf1,oper1,buf2); switch(oper1) { case '+': return atoi(buf1) + atoi(buf2); case '-': return atoi(buf1) - atoi(buf2); case '*': return atoi(buf1) * atoi(buf2); case '/': { int a = atoi(buf2); if(a) return atoi(buf1) / atoi(buf2); else return 0; } }}void cutereturn(char *s)//把字符串最后的回车字符吃掉{ int len = strlen(s); if(s[len - 1] == '\n') s[len - 1] = 0;}int main(){ FILE *p = fopen("D:\\main\\a.txt","r");//32+56=88 FILE *p1 = fopen("D:\\main\\b.txt","w"); char buf[1024]; char buf1[1024]; int value; while(!feof(p)) { memset(buf,0,sizeof(buf)); fgets(buf,sizeof(buf),p);//从文件中读取一行记录,字符串最后是以'\n'结尾的 cutereturn(buf);//吸收回车 value = calc_string(buf); memset(buf1,0,sizeof(buf1)); sprintf(buf1,"%s%d\n",buf,value);//将buf和buf计算结果重新组织成一个字符串 fputs(buf1,p1);//将重新组合后的字符串写入新的文件 } fclose(p); fclose(p1); return 0;}14.7 stat函数#include <sys/stat.h>int stat(const char *_Filename, struct stat * _Stat);stat.st_size;//文件大小,单位:字节函数的第一个参数代表文件名,第二个参数是struct stat结构。得到文件的属性,包括文件建立时间,文件大小等信息。Struct stat st = {0};//定义一个结构,名字叫stStat(“D:\\temp\\a.txt”,&st);//调用完stat函数之后,文件相关的信息就保存在了st结构中了14.8 fread和fwrite函数size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream);size_t fwrite(const void *ptr, size_t size, size_t nmemb,FILE *stream);注意:这个函数以二进制形式对文件进行操作,不局限于文本文件,可进行二进制的读写和拷贝返回值:返回实际写入的数据块数目size_t res=fread(buf,sizeof(char),sizeof(buf),p);//第一个参数是缓冲区,第二个参数是读取的时候最小单位大小,第三个参数是一次读几个单位,第四个参数是打开的文件指针,fread返回值代表读取了多少记录数fwrite(buf,sizeof(char),res,p1);//从源文件中读取多少字节,那么就往目标文件中写多少字节14.9 fread与feofwhile(!feof(p))//如果没有到达文件结尾,那么循环继续{ memset(buf,0,sizeof(buf));//每次读取文件一行之前都把这个buf清空 fgets(buf,sizeof(buf),p);//从文件中读取一行 fread(&buf, 1, sizeof(buf), p);}14.10 通过fwrite将结构保存到二进制文件中做一个代码例子
    14.11 fseek函数int fseek(FILE *stream, long offset, int whence);函数设置文件指针stream的位置。如果执行成功,stream将指向以fromwhere为基准,偏移offset(指针偏移量)个字节的位置,函数返回0。如果执行失败则不改变stream指向的位置,函数返回一个非0值。实验得出,超出文件末尾位置,还是返回0。往回偏移超出首位置,还是返回0,请小心使用。第一个参数stream为文件指针;第二个参数offest为偏移量,正数表示正向偏移,负数表示负向偏移;第三个参数whence设定从文件的哪里开始偏移,可能取值为:SEEK_CUR、SEEK_END或SEEK_SET。SEEK_SET:文件开头SEEK_CUR:当前位置SEEK_END:文件结尾fseek(fp, 3, SEEK_SET);
    14.12 ftell函数long ftell(FILE *stream);函数ftell用于得到文件位置指针当前位置相对于文件首的偏移字节数。在随机方式存取文件时,由于文件位置频繁的前后移动,程序不容易确定文件的当前位置。
    long len = ftell(fp);14.13 fflush函数int fflush(FILE *stream);fflush函数可以将缓冲区中任何未写入的数据写入文件中。成功返回0,失败返回EOF。每当程序通过C语言库函数往文件里面写数据,C语言库函数并不是实时的将数据直接写入磁盘,而是放到内存里面,当内存满了或者明确的调用了fclose,才将数据一次性写入磁盘。结合:C语言所有的文件操作函数都是缓冲区函数。
    fflush(p);//fflush将缓冲区的内容立刻写入文件//优势:不会因为停电,或者电脑死机等故障导致缓冲区的内容丢失;//不好:硬盘读写次数增加,导致程序效率低下,同时硬盘寿命变短。修改配置文件的时候,有时候会使用,或者做一些不经常修改的数据,但很重要数据,那么用fflush。
    14.14 remove函数int remove(const char *_Filename);remove函数删除指定文件参数Filename为指定的要删除的文件名,如果是windows下文件名与路径可以用反斜杠\分隔,也可以用斜杠/分隔。
    14.15 rename函数int rename(const char *_OldFilename, const char *_NewFilename);rename函数将指定文件改名参数OldFilename为指定的要修改的文件名,NewFilename为修改后的文件名,如果是windows下文件名与路径可以用反斜杠\分隔,也可以用斜杠/分隔。
    //程序还没有退出的时候,是不能同时打开很多文件的一个程序同时可以打开的文件数是有限的尽量在一个程序中不要同时打开太多的文件,如果确实要操作很多文件,也是一个操作完毕fclose以后,再去操作下一个文件。
    15 基础数据结构与算法15.1 什么是数据结构数据(data)是对客观事物符号表示,在计算机中是指所有能输入的计算机并被计算机程序处理的数据总称。数据元素(data element)是数据的基本单位,在计算机中通常作为一个整体进行处理。数据对象(data object)是性质相同的数据元素的集合,是数据的一个子集。数据结构(data structure)是相互之间存在一种或多种特定关系的数据元素的集合。数据类型(data type)是和数据结构密切关系的一个概念,在计算机语言中,每个变量、常量或者表达式都有一个所属的数据类型。抽象数据类型(abstract data type ADT)是指一个数据模型以及定义在该模型上的一组操作,抽象数据类型的定义仅取决于它的一组逻辑性,与其在计算机内部如何表示以及实现无关。
    15.2 什么是算法算法是对特定问题求解的一种描述,它是指令的有限序列,其每一条指令表示一个或多个操作,算法还有以下特性:

    有穷性一个算法必须总是在执行有限步骤后的结果,而且每一步都可以在有限时间内完成。
    确定性算法中每一个指令都有确切的含义,读者理解时不会产生二义性,在任何条件下,算法只有唯一的一条执行路径,即相同的输入只能得出相同的
    可行性一个算法是可行的,即算法中描述的操作都是可以通过已经实现的基本运算来实现的。
    输入一个算法有零个或者多个输入,这些输入取自与某个特定对象的集合。
    输出一个算法一个或多个输出,这些输出是和输入有某些特定关系的量。

    15.3 排序15.3.1 冒泡排序冒泡排序首先将一个记录的关键字和第二个记录的关键字进行比较,如果为逆序(elem[1]>elem[2]),则两个记录交换之,然后比较第二个记录和第三个记录的关键字,以此类推,直到第n-1个记录和第n个记录的关键字进行过比较为止。上述过程称作第一次冒泡排序,其结果是将关键字最大的记录接安排到最后一个记录的位置上,然后进行第二次冒泡排序,对前n-1个记录进行同样操作,其结果是使关键字第二大记录被安置到第n-1位置上,直到将所有记录都完成冒泡排序为止。
    #include <stdio.h>//冒泡排序void swap(int *a,int *b){ int tmp = *a; *a = *b; *b = tmp;}void bubble(int *array, int n){ int i; int j; for(i = 0; i < n; i++) for(j = 1; j < n-i; j++) { if(array[j - 1] > array[j]) { swap(&array[j - 1],&array[j]); } }}void print(int *array, int n){ int i; for(i = 0; i < n; i++) { printf("%d\n",array[i]); }}int main(void){ int array[10] = {32,45,8,78,21,89,4,15,23,56}; bubble(array,10); print(array,10); return 0;}15.3.2 选择排序选择排序是每一次在n-1+1(i=1,2,3,…n)个记录中选取关键字,最小的记录作为有序序列中第i个记录。通过n-1次关键字间的比较,从n-i+1个记录中选取出关键字最小的记录,并和第i(1<=i<=n)个记录交换之。
    int minkey(int *array, int low, int high)//查找指定范围内的最小值//第一个参数是一个数组,第二个参数是数组的开始下标,第三个参数是数组的终止下标//返回值是最小元素的下标{ int min = low; int key = array[low];//在没有查找最小元素之前,第一个元素是最小的 int i; for(i = low + 1; i < high; i++) { if(key > array[i]) { key = array[i]; min = i; } } return min;}void select(int *array, int n)//选择排序法{ int i; for(i = 0; i < n; i++) { int j = minkey(array, i, n); if(i != j)//范围内的第一个成员不是最小的 { swap(&array[i],&array[j]); } }}int main(void){ int array[10] = {32,45,8,78,21,89,4,15,23,56}; select(array,10); return 0;}15.4 查找15.4.1 顺序查找顺序查找的过程为:从表的最后一个记录开始,逐个进行记录的关键字和给定值比较,如果某个记录的关键字与给定值相等,则查找成功,反之则表明表中没有所查找记录,查找失败。
    int seq(int *array, int low, int high, int key)//顺序查找//在指定范围内寻找和key相同的值,找到返回下标,找不到返回—1{ int i; for(i = low; i < high; i++) { if(array[i] == key) return i; } return -1;}15.4.2 二分查找在一个已经排序的顺序表中查找,可以使用二分查找来实现。二分查找的过程是:先确定待查记录所在的范围(区间),然后逐步缩小查找范围,直到找到或者找不到该记录为止。假设指针low和high分别指示待查找的范围下届和上届,指针mid指示区间的中间值,即mid=(low + high) / 2。
    int bin(int *array, int low, int high, int key)//二分查找{ while(low <= high) { int mid = (low + high) / 2; if(key == array[mid])//中间切一刀,正好和要查找的数相等 return mid; else if(key > array[mid])//如果要找的数大于array[mid],那么就在下半部分继续切刀 low = mid + 1; else//如果要找的数小于array[mid],那么就在上半部分继续切刀 high = mid - 1; } return -1;//没有找到数据}int bin_rec(int *array, int low, int high, int key)//递归法实现二分查找{ if(low <= high) { int mid = (low + high) / 2; if(key == array[mid])//中间切一刀,正好和要查找的数相等 return mid; else if(key > array[mid])//下半部分继续查找 return bin_rec(array,mid + 1,high,key); else return bin_rec(array,low,mid - 1,key);//在上半部分查找 }else return -1;//没有找到数据}15.5 链表15.5.1 单向链表定义对于数组,逻辑关系上相邻的两个元素的物理位置也是相邻的,这种结构的优点是可以随机存储任意位置的元素,但缺点是如果从数组中间删除或插入元素时候,需要大量移动元素,效率不高。
    链式存储结构的特点,元素的存储单元可以是连续的,也可以是不连续的,因此为了表示每个元素a,与其接后的元素a+1之间的关系,对于元素a,除了存储其本身信息外,还需要存储一个指示其接后元素的位置。这两部分数据成为结点(node)。一个结点中存储的数据元素被称为数据域。存储接后存储位置的域叫做指针域。N个结点(ai(1<=i<=n))的存储映像链接成一个链表。
    整个链表必须从头结点开始进行,头结点的指针指向下一个结点的位置,最后一个结点的指针指向NULL;在链表中,通过指向接后结点位置的指针实现将链表中每个结点“链”到一起。链表中第一个结点称之为头结点。N个结点(ai的存储映像)链结成一个链表,即为线性表(a1,a2,…,an)的链式存储结构,因为此链表的每个结点中只包含一个指针域,所以叫做单链表。单链表正是通过每个结点的指针域将线性表的数据元素按其逻辑次序链接在一起。如图所示:
    15.5.2 单向链表数据结构定义struct list{ int data;//数据域 struct list *next;//指针域};15.5.3 单向链表的实现#include <stdio.h>#include <stdlib.h>struct list{ int data;//数据域 struct list *next;//指针域};struct list *create_list()//建立一个节点{ return calloc(sizeof(struct list),1);}struct list *insert_list(struct list *ls, int n, int data)//在指定位置插入元素{ struct list *p = ls; while(p && n--) { p = p->next; } if(p == NULL) { return NULL;//n的位置大于链表节点数 } struct list *node = create_list();//新建立一个节点 node->data = data; node->next = p->next; p->next = node; return node;}int delete_list(struct list *ls, int n)//删除指定位置元素{ struct list *p = ls; while(p && n--) { p = p->next; } if(p == NULL) { return -1;//n的位置不合适 } struct list *tmp = p->next; p->next = p->next->next; free(tmp); return 0;//删除成功}int count_list(struct list *ls)//返回链表元素个数{ struct list *p = ls; int count = 0; while(p) { count ++; p = p->next; } return count;}void clear_list(struct list *ls)//清空链表,只保留首节点{ struct list *p = ls->next; while(p) { struct list *tmp = p->next; free(p); p = tmp; } ls->next = NULL;//只有首节点,那么首节点的next也应该设置为NULL}int empty_list(struct list *ls)//返回链表是否为空{ if(ls->next) return 0; else return -1;}struct list *locale_list(struct list *ls, int n)//返回链表指定位置的节点{ struct list *p = ls; while(p && n--) { p = p->next; } if(p == NULL) return NULL; return p;}struct list *elem_locale(struct list *ls,int data)//返回数据域等于data的节点{ struct list *p = ls; while(p) { if(p->data == data) return p; p = p->next; } return NULL;//没有找到数据域等于data的节点}int elem_pos(struct list *ls, int data)//返回数据域等于data的节点位置{ int index = 0; struct list *p = ls; while(p) { index++; if(p->data == data) return index; p = p->next; } return -1;//没有找到数据域等于data的节点}struct list *last_list(struct list *ls)//得到链表最后一个节点{ struct list *p = ls; while(p->next) { p = p->next; } return p;}void merge_list(struct list *ls1,struct list *ls2)//合并两个链表,结果放入ls1中{ //只合并链表的节点,不合并链条头 last_list(ls1)->next = ls2->next; free(ls2);//链表头不要了}void reverse(struct list *ls)//链表逆置{ if (ls->next == NULL) return;//只有一个首节点,不需要逆置 if (ls->next->next == NULL) return;//也不需要逆置 struct list *last = ls->next;//逆置后ls->next就成了最后一个节点了 struct list *pre = ls;//上一个节点的指针 struct list *cur = ls->next;//当前节点的指针 struct list *next = NULL;//下一个节点的指针 while(cur) { next = cur->next; cur->next = pre; pre = cur; cur = next; } ls->next = pre; last->next = NULL;}void traverse(struct list *ls)//循环遍历链表{ struct list *p = ls; while(p) { printf("%d\n",p->data); p = p->next;//p指向他对应的下一个节点 }}int main(void){ struct list *first = create_list();//在堆中间创建一个节点 struct list *second = create_list();//在堆中间创建一个节点 struct list *third = create_list();//在堆中间创建一个节点 first->next = second; second->next = third; third->next = NULL;//对于链表的最后一个节点,next域一定为NULL first->data = 1; second->data = 2; third->data = 3; insert_list(first, 1, 10); insert_list(first, 1, 20); //delete_list(first, 2); //clear_list(first); traverse(first); printf("--------------\n"); printf("count = %d\n", count_list(first)); printf("%d\n", locale_list(first,3)->data); printf("data = %d\n",last_list(first)->data); printf("--------------\n"); struct list *first1 = create_list(); int i; for(i = 0; i < 10; i++) { insert_list(first1, 0, i); } merge_list(first,first1); printf("--------------\n"); traverse(first); printf("--------------\n"); reverse(first); traverse(first); return 0;}逆置操作

    判断首节点的next是否为NULL;
    判断第二个节点的next是否为NULL;
    逆置后ls->next就成了最后一个节点了
    最后一个节点的next指向NULL。
    0 留言 2019-07-27 10:48:37 奖励12点积分
  • windows下静态使用QxOrm框架并使用泛型编程 (三)

    这篇讲如何整合所有的表并且数据库增加字段升级,首先我们需要一张可以记录版本号的表
    VersionObject 这个类放置在QxObject里 因为字段不会增加所以我们只需要VersionHandler类即可 不需要映射类
    代码如下:VersionObject .h
    #ifndef VERSIONOBJECT_H#define VERSIONOBJECT_H#include "common.h"#include <QxOrm.h>#include <QString>class VersionObject{public: VersionObject(); void init();public: QString name; //long name; long version;};QX_REGISTER_PRIMARY_KEY(VersionObject, QString) //主键不是整数类型的时候使用QX_REGISTER_HPP_IMPORT_DLL(VersionObject, qx::trait::no_base_class_defined, DATABASE_VERSION)#endif // VERSIONOBJECT_HVersionObject .cpp
    #include "VersionObject.h"QX_REGISTER_CPP_IMPORT_DLL(VersionObject)namespace qx{template <> void register_class(QxClass<VersionObject> & t){ t.id(& VersionObject::name, "name"); t.data(& VersionObject::version, "version");}}VersionObject::VersionObject(){}void VersionObject::init(){ this->name= "qxorm"; this->version=DATABASE_VERSION;}VersionHandler.h
    #ifndef VERSIONHANDLER_H#define VERSIONHANDLER_H#include <QxOrm.h>#include "IHandler.h"#include "SQLModule/QxObject/VersionObject.h"namespace VERSION{const QString DATABASE_TYPE="QSQLITE";const QString CONNECT_NAME="VERSION_CONNECTED";const QString DATABASENAME="C:/Users/we/Desktop/workTools/demo/version.db";const QString HOSTNAME="localhost";const QString USERNAME="root";const QString PASSWORD="";}using namespace VERSION;class VersionObject;typedef QSharedPointer<VersionObject> Shared_Version;typedef QList<VersionObject> List_Version; //User类数组typedef qx::QxCollection<int,VersionObject> Collection_Version; //User容器class VersionHandler:public IHandler<Shared_Version,Collection_Version,VersionObject>,public ISqlInterface{public: VersionHandler(); virtual ~VersionHandler();protected: virtual void initSqlconnect(); virtual bool createTable(); virtual void disconnect();public: bool insert(Shared_Version &t); bool update(Shared_Version &t); bool select(Shared_Version &t);private: QSqlDatabase m_SqlDatabase; QMutex m_Mutex;};#endif // VERSIONHANDLER_HVersionHandler.cpp
    #include "VersionHandler.h"VersionHandler::VersionHandler(){ initSqlconnect(); if(createTable()) Shared_Version ersion(new VersionObject()); version->init(); this->insert(version); }}VersionHandler::~VersionHandler(){ disconnect();}void VersionHandler::initSqlconnect(){ QMutexLocker locker(&m_Mutex); if(QSqlDatabase::contains(CONNECT_NAME)) m_SqlDatabase = QSqlDatabase::database(CONNECT_NAME); else m_SqlDatabase= QSqlDatabase::addDatabase(DATABASE_TYPE,CONNECT_NAME); m_SqlDatabase.setDatabaseName(DATABASENAME); m_SqlDatabase.setHostName(HOSTNAME); m_SqlDatabase.setUserName(USERNAME); m_SqlDatabase.setPassword(PASSWORD); m_SqlDatabase.open();}bool VersionHandler::createTable(){ return IHandler<Shared_Version,Collection_Version,VersionObject>::createTable(m_SqlDatabase);}void VersionHandler::disconnect(){ QMutexLocker locker(&m_Mutex); if(m_SqlDatabase.isOpen()) m_SqlDatabase.close(); QSqlDatabase::removeDatabase(CONNECT_NAME);}bool VersionHandler::insert(Shared_Version &t){ return IHandler<Shared_Version,Collection_Version,VersionObject>::insert(t,m_Mutex,m_SqlDatabase);}bool VersionHandler::update(Shared_Version &t){ QStringList list; return IHandler<Shared_Version,Collection_Version,VersionObject>::update(t,m_Mutex,m_SqlDatabase,list);}bool VersionHandler::select(Shared_Version &t){ QStringList list; return IHandler<Shared_Version,Collection_Version,VersionObject>::select(t,m_Mutex,m_SqlDatabase,list);}为了更好的管理整个数据库的所有表 于是我们需要一个单例类来管理所有的Handler 所以我在SQLModule下新建了一个SQLHelper单例类
    代码如下:
    SqlHelper.h
    #ifndef SQLHELPER_H#define SQLHELPER_H#include <QMutex>#include "SQLModule/QxHandler/UserHandler.h"#include "SQLModule/QxHandler/VersionHandler.h"#include <QSqlDatabase>namespace SQLHELPER //用于版本升级的{const QString DATABASE_TYPE="QSQLITE";const QString CONNECT_NAME="UPDATE_ALLTABLE";const QString DATABASENAME="C:/Users/we/Desktop/workTools/demo/qxorm.db";const QString HOSTNAME="localhost";const QString USERNAME="root";const QString PASSWORD="";}class SqlHelper{public: static SqlHelper * getInstance(); bool init() bool isOldVersion(); //查询当前App 的数据库版本是否是以前的 如果是则调用updateDatabas void updateDatabase(); QSharedPointer<UserHandler> getUser();private: SqlHelper(){} SqlHelper(const SqlHelper&); SqlHelper& operator=(const SqlHelper); class CGarbo //单例自动回收 { public: CGarbo(){} ~CGarbo() if (SqlHelper::m_pSqlHelper) { delete SqlHelper::m_pSqlHelper; } } };private: static QMutex m_sMutex; static SqlHelper *m_pSqlHelper; static CGarbo m_sCGarbo;private: QSharedPointer<UserHandler> m_pUser; QSharedPointer<VersionHandler> m_pVersion;};#endif // SQLHELPER_HSqlHelper.cpp
    #include "SqlHelper.h"SqlHelper * SqlHelper ::m_pSqlHelper = nullptr;QMutex SqlHelper::m_sMutex;SqlHelper::CGarbo SqlHelper::m_sCGarbo;SqlHelper *SqlHelper::getInstance(){ SqlHelper* tmp = m_pSqlHelper; if (tmp == nullptr) { QMutexLocker lock(&m_sMutex); tmp = m_pSqlHelper; if (tmp == nullptr) { tmp = new SqlHelper(); m_pSqlHelper = tmp; } } return m_pSqlHelper;}bool SqlHelper::init(){ QSharedPointer<VersionHandler> tem1(new VersionHandler()); m_pVersion=tem1; QSharedPointer<UserHandler> tem2(new UserHandler()); m_pUser=tem2; return true;}bool SqlHelper::isOldVersion({ if(!m_pVersion.isNull()) { Shared_Version dbVersion(new VersionObject()); dbVersion->init(); m_pVersion->select(dbVersion); qDebug()<<"dbVersion->version"<<dbVersion->version; qDebug()<<" qApp->property()"<<qApp->property("DataBaseVersion").toInt(); qDebug()<<(dbVersion->version < qApp->property("DataBaseVersion").toInt()); return (dbVersion->version < qApp->property("DataBaseVersion").toInt()); } else { return false; }}void SqlHelper::updateDatabase(){ try { int dbversion=qApp->property("DataBaseVersion").toInt(); Shared_Version dbVersion(new VersionObject()); dbVersion->init(); m_pVersion->select(dbVersion); { QSqlDatabase db; if(QSqlDatabase::contains(SQLHELPER::CONNECT_NAME)) db = QSqlDatabase::database(SQLHELPER::CONNECT_NAME); else db= QSqlDatabase::addDatabase(SQLHELPER::DATABASE_TYPE,SQLHELPER::CONNECT_NAME); db.setDatabaseName(SQLHELPER::DATABASENAME); db.setHostName(SQLHELPER::HOSTNAME); db.setUserName(SQLHELPER::USERNAME); db.setPassword(SQLHELPER::PASSWORD); if(!db.isOpen()) { db.open(); } if (dbVersion->version >= dbversion) { if(db.isOpen()) { qDebug("**************** i come here ********************"); db.close(); } QSqlDatabase::removeDatabase(SQLHELPER::CONNECT_NAME); return; } QSqlQuery query(db); //获取在QxOrm注册的所有持久化类 qx::QxCollection<QString, qx::IxClass *> * pAllClasses = qx::QxClassX::getAllClasses(); if (! pAllClasses) { qAssert(false); return; } //将所有表获取到数据库中 QStringList tables = db.tables(); for (long k = 0; k < pAllClasses->count(); k++) { qx::IxClass * pClass = pAllClasses->getByIndex(k); if (! pClass) { continue; } // 过滤非persitents类 if (pClass->isKindOf("qx::service::IxParameter") || pClass->isKindOf("qx::service::IxService")) { continue; } // 筛选已经更新的类 if (pClass->getVersion() <= dbVersion->version) { continue; } qDebug()<<"****** pAllClasses->name ******"<<pClass->getName() <<pClass->getVersion(); // 如果表不存在,创建它并设置拥有者 if (! tables.contains(pClass->getName())) { qDebug()<<"***** want to creat table pClass->getName ******:"<<pClass->getName() query.exec("CREATE TABLE " + pClass->getName() + " ( ) WITH (OIDS = FALSE);" "ALTER TABLE " + pClass->getName() + " OWNER TO \"root\";"); //session += query.lastError(); } // 如果不存在列,则将其添加到表中 qx::IxDataMemberX * pDataMemberX = pClass->getDataMemberX(); for (long l = 0; (pDataMemberX && (l < pDataMemberX->count_WithDaoStrategy())); l++) { qx::IxDataMember * p = pDataMemberX->get_WithDaoStrategy(l); if (! p || (p->getVersion() <= dbVersion->version)){ continue; } qDebug()<<"***** add alter pClass->getName *****:"<<pClass->getName() <<p->getName()<<p->getSqlType(); query.exec("ALTER TABLE " + pClass->getName() + " ADD COLUMN " + p->getName() + " " + p->getSqlType() + ";"); //session += query.lastError(); if (p->getIsPrimaryKey()) // PRIMARY KEY { query.exec("ALTER TABLE " + pClass->getName() + " ADD PRIMARY KEY (" + p->getName() + ");"); //session += query.lastError(); } if (p->getAllPropertyBagKeys().contains("INDEX")) // INDEX { query.exec("CREATE INDEX " + pClass->getName() + "_" + p->getName() + "_idx" + " ON " + pClass->getName() + " USING " + p->getPropertyBag("INDEX").toString() + " (" + p->getName() + ");"); //session += query.lastError(); } if (p->getNotNull()) // NOT NULL { query.exec("ALTER TABLE " + pClass->getName() + " ALTER COLUMN " + p->getName() + " SET NOT NULL;"); //session += query.lastError(); } if (p->getAutoIncrement()) // AUTO INCREMENT { query.exec("CREATE SEQUENCE " + pClass->getName() + "_" + p->getName() + "_seq" + "; " "ALTER TABLE " + pClass->getName() + "_" + p->getName() + "_seq" + " OWNER TO \"root\"; " "ALTER TABLE " + pClass->getName() + " ALTER COLUMN " + p->getName() + " " + "SET DEFAULT nextval('" + pClass->getName() + "_" + p->getName() + "_seq" + "'::regclass);"); //session += query.lastError(); } if (p->getDescription() != "") // DESCRIPTION { query.exec("COMMENT ON COLUMN " + pClass->getName() + "." + p->getName() + " IS $$" + p->getDescription() + "$$ ;"); //session += query.lastError(); } } } //保存数据库的当前版本 dbVersion->version = dbversion; m_pVersion->update(dbVersion); if(db.isOpen()) { db.close(); } } QSqlDatabase::removeDatabase(SQLHELPER::CONNECT_NAME); } catch(const qx::dao::sql_error & err) { QSqlError sqlError = err.get(); qDebug() << sqlError.databaseText(); qDebug() << sqlError.driverText(); qDebug() << sqlError.number(); qDebug() << sqlError.type(); }}QSharedPointer<UserHandler> SqlHelper::getUser(){ return m_pUser;}是否有发现SQLHelper 里面有两个方法 isOldVersion() 和updateDatabase() 这个是用于校验当前app版本和数据库版本的方法,直接照抄就可以了修改官方并且验证过了。
    还有个common.h
    #ifndef COMMON_H#define COMMON_H#include <QString>const QString DATABASE_APP="DataBaseVersion";const int DATABASE_VERSION=0; //数据库版本控制#endif // COMMON_Hmain.cpp
    #include <QApplication>#include "common.h"int main(int argc, char *argv[]){ QApplication a(argc, argv); a.setProperty("DataBaseVersion",DATABASE_VERSION); return a.exec();}github源码:https://github.com/qq2690351079/ontheway 如果没有 就是我还没整理完还没上传
    后续会发出模板类的MVP 框架QT写的 不过改改C++也应该能直接用。
    数据库记得做DB 每次更新DB以后再update。
    0 留言 2019-07-23 14:39:54 奖励5点积分
  • 【Cocos Creator 联机实战教程(2)】——匹配系统 精华

    1.知识点讲解大型多人互动游戏一般都会有一个匹配系统,用来让玩家进行联网游戏,现在我们来讲一讲这种系统吧,我们可以做个比较简单的双人对战匹配系统。
    我们让每一对匹配成功的玩家进入一个独立的房间,所以不同的房间的通信应该互不影响,由于不同场景的通信内容不同,所以不同场景的通信也应该独立
    我们把这个游戏的匹配过程比作开房的过程,

    如果有一个人进入了宾馆,那么他最先进入的区域就是hall(大厅),当然他可能就是逛逛,又推门出去
    当他想休息时他就去前台开个房,那么他就进入了queue(队列),并断开hall的通信
    当另一个人也想休息的时候也去前台排队,当个queue里有两个人的时候,前台小姐就给了他俩一个空闲房间的钥匙,他们就一起进入了一个独立的room,并断开queue的通信
    以上循环,房间数有限,在房间满的时候不能匹配成功

    当然,你也可以根据实际情况升级这个匹配系统,比如,分等级的匹配(开不同的队列等待)。
    注意:房卡游戏虽然也用到了房间这个概念,但不是匹配,这种游戏更像唱卡拉OK。进入大厅后,组织者去开个房间,其他人一起进。或者迟到的人拿着房间号直接进去。
    2. 步骤我们的游戏分为三个场景

    游戏启动的时候进入menu场景,当玩家点击对战时进入match场景,匹配成功进入game场景,取消匹配返回menu场景,游戏结束返回menu场景
    我们在Global里定义socket
    window.G = { globalSocket:null,//全局 hallSocket:null,//大厅 queueSocket:null,//队列 roomSocket:null,//房间 gameManager:null, chessManager:null, stand:null,}
    menu场景启动时,我们连接hallSocket,开始匹配时,断开hallSocket
    cc.Class({ extends: cc.Component, onLoad: function () { G.globalSocket = io.connect('127.0.0.1:4747'); //断开连接后再重新连接需要加上{'force new connection': true} G.hallSocket = io.connect('127.0.0.1:4747/hall',{'force new connection': true}); }, onBtnStart() { G.hallSocket.disconnect(); cc.director.loadScene('match'); }});
    进入match场景,连接queueSocket,先进入queue的玩家主场黑棋先手,后进入客场白棋后手(这个逻辑是服务端判断的),匹配成功时,服务端会发送roomId,玩家进入相应的房间,并断开queueSocket的通信
    const Constants = require('Constants');const STAND = Constants.STAND;cc.Class({ extends: cc.Component, onLoad: function () { G.queueSocket = io.connect('127.0.0.1:4747/queue', { 'force new connection': true }); G.queueSocket.on('set stand', function (stand) { if (stand === 'black') { G.stand = STAND.BLACK; } else if (stand === 'white') { G.stand = STAND.WHITE; } }); G.queueSocket.on('match success', function (roomId) { cc.log('match success' + roomId); G.roomSocket = io.connect('127.0.0.1:4747/rooms' + roomId, { 'force new connection': true }); G.queueSocket.disconnect(); cc.director.loadScene('game'); }); }, onBtnCancel() { G.queueSocket.disconnect(); cc.director.loadScene('menu'); }});
    在game场景中,如果游戏结束我们就断掉roomSocket回到menu场景
    startGame() { this.turn = STAND.BLACK; this.gameState = GAME_STATE.PLAYING; this.showInfo('start game'); },endGame() { let onFinished = () =>{ G.roomSocket.disconnect(); cc.director.loadScene('menu'); } this.infoAnimation.on('finished',onFinished,this); this.gameState = GAME_STATE.OVER; this.showInfo('game over'); },
    服务端完整逻辑
    let app = require('express')();let server = require('http').Server(app);let io = require('socket.io')(server);server.listen(4747, function() { console.log('listening on:4747');});let MAX = 30;//最大支持连接房间数let hall = null;//大厅let queue = null;//匹配队列let rooms = [];//游戏房间function Hall() { this.people = 0; this.socket = null;}function Room(){ this.people = 0; this.socket = null;}function Queue(){ this.people = 0; this.socket = null;}hall = new Hall();queue = new Queue();for(let n = 0;n < MAX;n++){ rooms[n] = new Room();}function getFreeRoom(){ for(let n = 0;n < MAX;n++){ if(rooms[n].people === 0){ return n; } } return -1;}io.people = 0;io.on('connection',function(socket){ io.people++; console.log('someone connected'); socket.on('disconnect',function(){ io.people--; console.log('someone disconnected'); });})hall.socket = io.of('/hall').on('connection', function(socket) { hall.people++; console.log('a player connected.There are '+hall.people+' people in hall'); hall.socket.emit('people changed',hall.people); socket.on('disconnect',function(){ hall.people--; console.log('a player disconnected.There are '+hall.people+' people in hall'); hall.socket.emit('people changed',hall.people); });});queue.socket = io.of('/queue').on('connection',function(socket){ queue.people++; console.log('someone connect queue socket.There are '+queue.people+' people in queue'); if(queue.people === 1){ socket.emit('set stand','black'); }else if(queue.people === 2){ socket.emit('set stand','white'); let roomId = getFreeRoom(); console.log(roomId+"roomId"); if(roomId >= 0){ queue.socket.emit('match success',roomId); console.log('match success.There are '+queue.people+' people in queue'); }else{ console.log('no free room!'); } } socket.on('cancel match',function(){ queue.people--; console.log('someone cancel match.There are '+queue.people+' people in queue'); }); socket.on('disconnect',function(){ queue.people--; console.log('someone disconnected match.There are '+queue.people+' people in queue'); });});for(let i = 0;i < MAX;i++){ rooms[i].socket = io.of('/rooms'+i).on('connection',function(socket){ rooms[i].people++; console.log('some one connected room'+i+'.There are '+rooms[i].people+' people in the room'); socket.on('update chessboard',function(chessCoor){ socket.broadcast.emit('update chessboard',chessCoor); }); socket.on('force change turn',function(){ socket.broadcast.emit('force change turn'); }); socket.on('disconnect',function(){ rooms[i].people--; console.log('someone disconnected room'+i+'.There are '+rooms[i].people+' people in the room'); }); });}
    3. 总结我们做的是比较简单的匹配系统,实际上还有匹配算法(选择排队的顺序不仅仅是先来后到)。
    这是我们需要掌握的新知识,除此之外我们都可以使用之前的知识点完成游戏。
    注意以下问题:

    跨场景访问变量
    在util下面有两个脚本,Constants用来存储游戏常量,然后其他地方需要常量时
    const Constants = require('Constants');const GAME_STATE = Constants.GAME_STATE;const STAND = Constants.STAND;const CHESS_TYPE = Constants.CHESS_TYPE;
    Global存储全局控制句柄,需要访问他们的时候,就可以通过(G.)的方式

    控制单位应该是脚本而不是节点
    本教程部分素材来源于网络。
    3 留言 2018-12-07 14:58:43 奖励35点积分
  • 中山大学智慧健康服务平台应用开发-基本的UI界面设计

    一、实验题目实验一: 中山大学智慧健康服务平台应用开发
    二、实现内容2.1 基本的UI界面设计实现一个Android应用,界面呈现如图中的效果。

    2.1.1 要求
    该界面为应用启动后看到的第一个界面
    各控件的要求

    标题字体大小20sp,与顶部距离20dp,居中图片与上下控件的间距均为20dp,居中输入框整体距左右屏幕各间距20dp,内容(包括提示内容)如图所示,内容字体大小18sp按钮与输入框间距10dp,文字大小18sp。按钮背景框左右边框与文字间距10dp,上下边框与文字间距5dp,圆角半径180dp,背景色为#3F51B5四个单选按钮整体居中,与输入框间距10dp,字体大小18sp,各个单选按钮之间间距10dp,默认选中的按钮为第一个

    2.1.2 使用的组件TextView、EditText、ConstraintLayout、Button、ImageView、RadioGroup、RadioButton。实现一个Android应用,界面呈现如图中的效果。
    2.1.3 验收内容
    各控件的位置,间距,字体大小等属性与要求无误
    图片大小不作为验收内容给之一

    2.2 事件处理
    2.2.1 要求
    该界面为应用启动后看到的第一个界面
    各控件处理的要求

    点击搜索按钮
    如果搜索内容为空,弹出Toast信息“搜索内容不能为空”如果搜索内容为“Health”,根据选中的RadioButton项弹出如下对话框点击“确定”,弹出Toast信息——对话框“确定”按钮被点击点击“取消”,弹出Toast 信息——对话框“取消”按钮被点击否则弹出如下对话框,对话框点击效果同上

    RadioButton选择项切换:选择项切换之后,弹出Toast信息“XX被选中”,例如从图片切换到视频,弹出Toast信息“视频被选中”

    2.2.2 验收内容
    布局是否正常
    搜索内容为空时,提示是否正常
    输入搜索内容后,点击搜索按钮是否能根据不同的搜索内容显示相应的弹出框,以及弹出框内容是否符合要求
    点击弹出框的相应按钮是否能提示正确的内容
    RadioButton切换时,提示是否正常

    2.3 Intent、Bundle的使用以及RecyclerView、ListView的应用本次实验模拟实现一个健康食品列表,有两个界面,第一个界面用于呈现食品列表如下所示。

    数据在”manual/素材”目录下给出。
    点击右下方的悬浮按钮可以切换到收藏夹。

    上面两个列表点击任意一项后,可以看到详细的信息:

    2.3.1 UI要求食品列表
    每一项为一个圆圈和一个名字,圆圈和名字都是垂直居中。圆圈内的内容是该食品的种类,内容要处于圆圈的中心,颜色为白色。食品名字为黑色,圆圈颜色自定义,只需能看见圆圈内的内容即可。
    收藏夹
    与食品列表相似。
    食品详情界面
    界面顶部

    顶部占整个界面的1/3。每个食品详情的顶部颜色在数据中已给出。返回图标处于这块区域的左上角,食品名字处于左下角,星标处于右下角,边距可以自己设置。 返回图标与名字左对齐,名字与星标底边对齐。 建议用RelativeLayout实现,以熟悉RelativeLayout的使用。
    界面中部

    使用的黑色argb编码值为#D5000000,稍微偏灰色的“富含”“蛋白质”的argb编码值为#8A000000。”更多资料”一栏上方有一条分割线,argb编码值为#1E000000。右边收藏符号的左边也有一条分割线,要求与收藏符号高度一致,垂直居中。字体大小自定。”更多资料”下方分割线高度自定。这部分所有的分割线argb编码值都是#1E000000。
    界面底部

    使用的黑色argb编码值为#D5000000。
    标题栏
    两个界面的标题栏都需要去掉。
    2.3.2 功能要求使用RecyclerView实现食品列表。点击某个食品会跳转到该食品的详情界面,呈现该食品的详细信息。长按列表中某个食品会删除该食品,并弹出Toast,提示 “删除XX” 。
    点击右下方的FloatingActionButton,从食品列表切换到收藏夹或从收藏夹切换到食品列表,并且该按钮的图片作出相应改变。
    使用ListView实现收藏夹。点击收藏夹的某个食品会跳转到食品详情界面,呈现该食品的详细信息。长按收藏夹中的某个食品会弹出对话框询问是否移出该食品,点击确定则移除该食品,点击取消则对话框消失。
    商品详情界面中点击返回图标会返回上一层。点击星标会切换状态,如果原本是空心星星,则会变成实心星星;原本是实心星星,则会变成空心星星。点击收藏图表则将该食品添加到收藏夹并弹出Toast提示 “已收藏” 。
    三、实验结果3.1 基本的UI界面设计与基础事件处理3.1.1 实验截图切换按钮时候,显示当前切换到的按钮名字,如下图,视频被选中:

    搜索Health关键词时,显示对话框搜索成功:

    搜索其他关键词,无法正确搜索,显示搜索错误对话框:

    点击取消按钮时,显示toast取消被单击:

    3.1.2 实验步骤以及关键代码这个实验前两部分包括简单的UI设计以及UI的交互。
    首先,我们当然要从UI的构建开始。
    1.插入标题以及图片这里应用到了TextView以及ImageView两个控件。由于本次的ui是使用ConstraintLayout布局,所以必须对每一个控件设置左右上下分别对齐什么。故要利用app:layout_constraintLeft_toLeftOf 等属性,表示该组件的左边对齐于xx的左边,这里的textview就要与parent即整个页面的左边对齐,然后设置居中。宽度,大小就根据实验要求来设置,而id是用于后面的交互部分识别该控件用的。
    <TextView android:id="@+id/title" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@string/title" android:textSize="20sp" app:layout_constraintTop_toTopOf="parent" android:layout_marginTop="20dp" app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintRight_toRightOf="parent" /> <ImageView android:id="@+id/image" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginTop="20dp" android:src="@mipmap/sysu" app:layout_constraintBottom_toTopOf="@+id/search_content" app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintRight_toRightOf="parent" app:layout_constraintTop_toBottomOf="@id/title" />
    2.插入搜索输入框以及搜索按钮对于输入框要使用EditText控件,对于按钮要使用Button控件。对于输入框的显示内容,预先在string文件中写入,然后直接在控件中调用即可。对于button还用到了style属性,表示直接引用style写好的按钮样式。而style里面又调用了其他文件中已经预设好的属性,例如color中颜色。
    <style name="search_button"> <item name="android:textColor">@color/white</item> <item name="android:background">@drawable/button</item> </style>
    <EditText android:id="@+id/search_content" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_marginLeft="20dp" android:layout_marginRight="10dp" android:layout_marginTop="20dp" android:gravity="center" android:hint="@string/search_content" android:textSize="18sp" app:layout_constraintBottom_toTopOf="@+id/radioGroup" app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintRight_toLeftOf="@+id/but1" app:layout_constraintTop_toBottomOf="@id/image" /> <Button android:id="@+id/but1" style="@style/search_button" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginRight="20dp" android:layout_marginTop="20dp" android:text="@string/search_button" android:textSize="18sp" app:layout_constraintBottom_toTopOf="@+id/radioGroup" app:layout_constraintRight_toRightOf="parent" app:layout_constraintTop_toBottomOf="@id/image" />
    3. 插入选择按钮选择按钮组要使用RadioGroup与RadioButton相配合,在group中设置边距以及大小,对于每一个radiobutton使用其他设置好的样式属性,在第一个选择按钮中设置checked属性设置为true就会默认第一个按钮被选定。
    <RadioGroup android:id="@+id/radioGroup" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginTop="10dp" android:orientation="horizontal" app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintRight_toRightOf="parent" app:layout_constraintTop_toBottomOf="@id/search_content"> <RadioButton android:id="@+id/selection1" style="@style/MyRadioButton" android:layout_height="match_parent" android:checked="true" android:text="@string/selection1" /> <RadioButton android:id="@+id/selection2" style="@style/MyRadioButton" android:text="@string/selection2" /> <RadioButton android:id="@+id/selection3" style="@style/MyRadioButton" android:text="@string/selection3" /> <RadioButton android:id="@+id/selection4" style="@style/MyRadioButton" android:text="@string/selection4" /> </RadioGroup>
    这就基本完成了UI的界面设置,接下来要根据他们的id来设置一些函数实现实验要求,例如弹出对话框或者toast等等。
    4.获取搜索输入框的内容,以及点击搜索按钮显示提示这一步主要要调用findViewById这个函数来分别得到输入框以及按钮,给按钮设置监听函数setOnClickListener, 然后在里面对于输入框的内容searchContent.getText().toString()来进行判断,分别有三种情况,搜索内容为空,搜索内容为Health,搜索内容为其他。
    然后,关于对话框的显示要使用dialog,分别给它设置标题,中间内容以及按钮。而toast则要对于对话框的按钮来设置监听函数,当点击时候来Toast.makeText()显示一个具体的toast内容。
    Button button =(Button) findViewById(R.id.but1); final EditText searchContent = (EditText) findViewById(R.id.search_content); button.setOnClickListener(new View.OnClickListener(){ @Override public void onClick(View view){ //搜索为空情况 if(TextUtils.isEmpty(searchContent.getText().toString())){ //弹出 Toast Toast.makeText(MainActivity.this, "搜索内容不能为空",Toast.LENGTH_SHORT).show(); } //搜索成功情况 else if(searchContent.getText().toString().equals("Health")){ AlertDialog.Builder dialog = new AlertDialog.Builder(MainActivity.this); dialog.setTitle("提示"); RadioButton temp = findViewById(radioGroup.getCheckedRadioButtonId()); dialog.setMessage(temp.getText().toString()+"搜索成功"); dialog.setPositiveButton("确定", new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialogInterface, int i) { Toast.makeText(MainActivity.this, "对话框“确定”按钮被点击",Toast.LENGTH_SHORT).show(); } }); dialog.setNegativeButton("取消", new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialogInterface, int i) { Toast.makeText(MainActivity.this, "对话框“取消”按钮被点击",Toast.LENGTH_SHORT).show(); } }); dialog.show(); } //搜索失败情况 else{ AlertDialog.Builder dialog = new AlertDialog.Builder(MainActivity.this); dialog.setTitle("提示"); dialog.setMessage("搜索失败"); dialog.setPositiveButton("确定", new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialogInterface, int i) { Toast.makeText(MainActivity.this, "对话框“确定”按钮被点击",Toast.LENGTH_SHORT).show(); } }); dialog.setNegativeButton("取消", new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialogInterface, int i) { Toast.makeText(MainActivity.this, "对话框“取消”按钮被点击",Toast.LENGTH_SHORT).show(); } }); dialog.show(); } } });
    4.对于选择按钮组的切换与上面相同,先要通过id来找到相应的控件,然后对于radioGroup来设置选择改变的监听函数,当切换的时候会根据选择的不同按钮上的信息来生成一个toast。
    final RadioGroup radioGroup = findViewById(R.id.radioGroup); final RadioButton radioButton = findViewById(radioGroup.getCheckedRadioButtonId()); radioGroup.setOnCheckedChangeListener(new RadioGroup.OnCheckedChangeListener(){ @Override //选择变化时,弹出toast提示信息 public void onCheckedChanged(RadioGroup group, int checkedID){ String str = ""; RadioButton select1 = findViewById(R.id.selection1); RadioButton select2 = findViewById(R.id.selection2); RadioButton select3 = findViewById(R.id.selection3); RadioButton select4 = findViewById(R.id.selection4); if(select1.getId() == checkedID){ str = select1.getText().toString(); } else if(select2.getId() == checkedID){ str = select2.getText().toString(); } else if(select3.getId() == checkedID){ str = select3.getText().toString(); } else if(select4.getId() == checkedID){ str = select4.getText().toString(); } Toast.makeText(MainActivity.this, str + "被选中",Toast.LENGTH_SHORT).show(); } });
    3.1.3 实验遇到的困难以及解决思路1.关于UI部分的边距问题起初对于ConstraintLayout布局不熟悉,不理解为什么需要对于一个控件的左右边限制跟随另一个的左右边,单纯认为只需要改变margin即可完成布局。而实际情况时,根据布局出来的结果可以看到仅改变margin之后相对于父亲来改变距离,而不能完全地设置两个组件的相应距离。于是完成一个组件时候,对于下一个组件的上下左右边缘要根据相对应的组件来限制一下。
    而在修改UI的时候,多使用preview功能以及在xml下切换至design模式,可以清晰看出组件之间的边距关系,查看布局是否正确。

    2.如何让中间的搜索框以及搜索按钮以合适的大小安放在同一行?这个问题就是在ui部分一直困扰我的,由于搜索框与左边要有限制,在右边又要与搜索按钮有限制,而搜索框也要与右边有限制。这样设置 app:layout_constraintRight_toRightOf 等等需要十分注意。
    而且输入框的长度也要合适,当 android:layout_width=”wrap_parent” 时候仅显示了提示内容的长度。而 android:layout_width=”fill_parent” 时候又占满了整个显示屏,显然是不行。而选择固定长度则不符合我们安卓手机界面设计的原则,无法在各种机型中显示合理。
    经过查询多种资料,可以通过设置 android:layout_width=”0dp” 来使这个输入框自适应边距,因此问题迎刃而解。
    3.实现交互部分的api比较通用的找到控件的函数为findViewById,通过id来找到控件,这与我们设置的id就很关键了,必须要注意大小写以及名字的正确性。
    关于组件的监听函数,包括点击按钮,切换radiobutton等等,都要了解其中的参数,查看手册。
    3.2 Intent、Bundle的使用以及RecyclerView、ListView的应用3.2.1 实验截图下图为食物列表的展示,浮标图案为前往收藏夹:

    下图为收藏夹初始页面的展示,浮标图案为返回主页样式:

    下图为大豆食物的详情信息:

    下图为点击星星以及收藏按钮产生的事件截图:

    下图为收藏大豆事件后,收藏夹的信息截图:

    下图为长按大豆列表删除时的操作截图:

    下图为在食物列表长按食物删除的操作截图:

    3.2.2 实验步骤以及关键代码本次实验的内容有点多,要完成三个页面的设计以及不同活动之间的信息交互。
    1.完成从搜索页面跳转到FoodList页面由于上次的实验中完成了一个搜索的界面,我为了将两次实验连接到一起,因此在搜索页面搜索switch时候会跳转到食物列表页面(即本次实验内容)
    要记得在mainfest中注册该活动,否则会出现应用闪退的现象,下面的两个页面也是如此,不再详述。

    这里使用startActivity以及intent来实现页面的跳转。
    ...//切换至食物列表,第二周任务的衔接第一周任务else if(searchContent.getText().toString().equals("switch")){ Intent intent = new Intent(); intent.setClass(MainActivity.this, FoodList.class); startActivity(intent);}...
    2.存储食物数据为了保存这些食物数据,我新建了一个MyCollection类来存储,类函数包括构造函数以及各个参数的get,set函数,不必详述。
    public class MyCollection implements Serializable { private String name; //食物名字 private String content; //食物图标 private String type; //食物种类 private String material; //食物成分 private boolean is_collected; //是否被收藏 private boolean is_star; //是否被加星 public MyCollection(){ is_collected =false; } public MyCollection(String _name, String _content, String _type, String _material, boolean _is_star){ name = _name; content = _content; type =_type; material = _material; is_star = _is_star; is_collected = false; } ... //各种get,set函数
    3.利用RecycleView实现FoodList这一部分可以说是这次实验的难点,我用了一天的时间才能理解RecycleView的实现过程。一个RecycleView需要一个Adater以及一个Holder来实现,存储的数据利用Holder,而用户点击的事件则利用Adater.
    首先实现MyViewHolder类,它必须继承RecyclerView.ViewHolder。其中通过findViewById函数来查找列表的填充项,如果已经查找过了就从数组中直接拿出即可,这样可以加快应用的速度,优化性能。
    public class MyViewHolder extends RecyclerView.ViewHolder { private SparseArray<View> views; private View view; public MyViewHolder(View _view) { super(_view); view = _view; views = new SparseArray<View>(); } public <T extends View> T getView(int _viewId) { View _view = views.get(_viewId); if (_view == null) { //创建view _view = view.findViewById(_viewId); //将view存入views views.put(_viewId, _view); } return (T) _view; }}
    接着是MyRecyclerViewAdapter类,它必须继承RecyclerView.Adapter类,其中利用MyViewHolder来存储列表的数据。该类实现点击的功能,这里新建了item的点击监听器,包括单击以及长按两种操作。
    除此之外,它必须重构onCreateViewHolder,onBindViewHolder,getItemCount这三个函数。
    在onBindViewHolder中为item来重构点击事件,其中长按事件函数要返回false,不然会与单击事件同时触发
    public class MyRecyclerViewAdapter<T> extends RecyclerView.Adapter<MyViewHolder>{ private List<MyCollection> data; private Context context; private int layoutId; private OnItemClickListener onItemClickListener; public MyRecyclerViewAdapter(Context _context, int _layoutId, List<MyCollection> _data){ context = _context; layoutId = _layoutId; data = _data; } //点击事件的接口 public interface OnItemClickListener{ void onClick(int position); void onLongClick(int position); } public void setOnItemClickListener(OnItemClickListener _onItemClickListener) { this.onItemClickListener = _onItemClickListener; } //删除数据 public void deleteData(int position){ data.remove(position); } @Override public MyViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { MyViewHolder holder = new MyViewHolder(LayoutInflater.from(context).inflate(R.layout.item, parent, false)); return holder; } @Override public void onBindViewHolder(final MyViewHolder holder, int position) { //convert ((TextView)holder.getView(R.id.recipeName)).setText(data.get(position).getName()); ((TextView)holder.getView(R.id.img)).setText(data.get(position).getContent()); if (onItemClickListener != null) { //单击 holder.itemView.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { onItemClickListener.onClick(holder.getAdapterPosition()); } }); //长按 holder.itemView.setOnLongClickListener(new View.OnLongClickListener() { @Override public boolean onLongClick(View view) { onItemClickListener.onLongClick(holder.getAdapterPosition()); return false; } }); } } @Override public int getItemCount() { if(!data.isEmpty()) return data.size(); return 0; }}
    我们先在FoodList.xml布局文件中预先设置了recycleview,以及新建一个item.xml来初始化列表项,包括两个textView组件来存放食物的标志以及文字。
    在FoodList.java中来通过recycleview的id来找到该组件,然后来通过adapter来设置。首先要利用setLatoutManager函数类似ListView来设置layout。
    然后设置监听器,单击跳转到详情页面根据点击的位置gotoDetail_for_Foodlist(position);该函数在后面部分叙述,此处只需知道它跳转到了详情页面。
    而长按时,要将数据删除,这里使用notifyItemRemoved(position);以及之前在Adapter实现了的删除函数来实现这一功能,最后弹出一个toast。
    RecyclerView recyclerView = findViewById(R.id.recyclerView); recyclerView.setLayoutManager(new LinearLayoutManager(FoodList.this)); // 类似ListView final MyRecyclerViewAdapter myAdapter = new MyRecyclerViewAdapter<MyCollection>(FoodList.this, R.layout.item, data2); myAdapter.setOnItemClickListener(new MyRecyclerViewAdapter.OnItemClickListener() { @Override public void onClick(int position) { gotoDetail_for_Foodlist(position); } @Override public void onLongClick(int position) { myAdapter.notifyItemRemoved(position); myAdapter.deleteData(position); Toast.makeText(FoodList.this,"移除第"+(position+1)+"个商品", Toast.LENGTH_SHORT).show(); } }); recyclerView.setAdapter(myAdapter); //不使用动画情况,后面为其加自定义动画,见实验思考内容
    到这里,我就完成了第一个页面FoodList中列表的设计,但是还需要一个浮动按键。根据实验的教程来引入依赖后,在FoodList.xml为其新建组件,设定id。
    然后在食物列表页面通过id找到按键来处理,这里要求改变图片以及展示的内容,需要用到setVisibility,setImageResource这两个函数,通过一个tag来确定显示哪个页面,然后通过设置其是否展示或者展示哪张图片即可。
    //点击浮标事件 final FloatingActionButton f_but = findViewById(R.id.btn); f_but.setOnClickListener(new View.OnClickListener(){ boolean tag_for_foodlist = true; @Override public void onClick(View v){ if(tag_for_foodlist){ findViewById(R.id.recyclerView).setVisibility(View.GONE);//设置Foodlist不可见 findViewById(R.id.listView).setVisibility(View.VISIBLE); tag_for_foodlist = false; f_but.setImageResource(R.mipmap.mainpage); } else{ findViewById(R.id.recyclerView).setVisibility(View.VISIBLE); findViewById(R.id.listView).setVisibility(View.GONE);//设置Favourite不可见 tag_for_foodlist = true; f_but.setImageResource(R.mipmap.collect); } } });
    4.利用ListView实现Collection收藏夹页面首先在FoodList建立listview组件,然后才能通过id来找到。
    ListView就比前面简单很多,可以直接使用simpleAdapter来直接设置,只需调整传入的内容参数即可,点击的监听器要分别设单击事件,前往详情页面;长按事件,弹出询问框是否删除,这一部分是上一实验的内容不再详述。
    //ListView部分 ListView listview = (ListView) findViewById(R.id.listView); simpleAdapter = new SimpleAdapter(this, favourite, R.layout.item, new String[] {"img", "recipeName"}, new int[] {R.id.img, R.id.recipeName}); listview.setAdapter(simpleAdapter); listview.setOnItemClickListener(new AdapterView.OnItemClickListener() { @Override public void onItemClick(AdapterView<?> adapterView, View view, int i, long l) { if(i != 0) gotoDetail_for_Collect(i); } }); listview.setOnItemLongClickListener(new AdapterView.OnItemLongClickListener() { @Override public boolean onItemLongClick(AdapterView<?> adapterView, View view, int i, long l) { // 处理长按事件 if(i != 0){ //弹出询问框 final int delete_num = i; AlertDialog.Builder dialog = new AlertDialog.Builder(FoodList.this); dialog.setTitle("删除"); dialog.setMessage("确定删除"+favourite.get(i).get("recipeName")+"?"); dialog.setPositiveButton("确定", new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialogInterface, int i) { favourite.remove(delete_num); simpleAdapter.notifyDataSetChanged(); } }); dialog.setNegativeButton("取消", new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialogInterface, int i) { } }); dialog.show(); } return true; //这样长按与短按不会同时触发 } });
    5.利用Relative布局,Linear布局,Constraint布局实现Detail详情页面的UI部分这一部分需要我了解各种布局的一些具体情况,比如如何设置水平垂直居中,如何设置三分之一,如何与别的组件保持在一水平线上等等。
    因为上次实验已经使用了Constraint布局来设计UI,所以这里只分析一下对于用Relative布局的详情页面的顶部,要将顶部设置为三分之一,需要利用android:layout_weight=”1”这一属性,需要注意的是使用这一属性时,必须将高度设置为0,让其自动来匹配页面,以达成三分之一的效果。
    对于RelativeLayout布局,layout_alignParentLeft表示返回图标位于页面的左侧,其次食物名字要与返回图标的左侧对齐就要使用android:layout_alignLeft=”@id/back”,里面的参数为想要对齐的id。
    对于星星图标的处理可以预先设置为空星星,而且增加tag来为后面的变化做准备。
    而我为了保存星星的状态,不使用这一方法,所以在xml上不写图片,而在Detail.xml根据食物来动态生成。
    <RelativeLayout android:id="@+id/top" android:layout_width="match_parent" android:layout_height="0dp" android:background="#3F51BB" android:layout_weight="1" > <ImageView android:id="@+id/back" android:layout_width="50dp" android:layout_height="50dp" android:src="@mipmap/back" android:layout_alignParentLeft="true" android:layout_marginTop="10dp" android:layout_marginStart="10dp" android:layout_marginLeft="10dp" /> <TextView android:id="@+id/name" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_alignParentBottom="true" android:layout_marginBottom="20dp" android:layout_marginRight="20dp" android:text="牛奶" android:textSize="30dp" android:textColor="@color/white" android:layout_alignLeft="@id/back" android:layout_marginEnd="10dp" /> <ImageView android:id="@+id/star" android:layout_width="40dp" android:layout_height="40dp" android:layout_alignParentEnd="true" android:layout_alignTop="@+id/name" android:layout_marginEnd="20dp" android:layout_marginRight="20dp" android:layout_alignParentRight="true" /> </RelativeLayout>
    6.利用Intent,startActivity等实现不同活动之间的传递信息从前面点击监听器所绑定的跳转函数开始说明,这里是跳转到详情食物页面的函数,它必须根据坐标来将MyCollection中的内容读取出来,并将其放到bundle中,利用startActivityForResult来跳转到详情页面并等待返回参数来进行处理。这里需要的处理包括星星事件的点击,已经加入收藏夹的事件。
    private void gotoDetail_for_Foodlist(int position){ Intent intent = new Intent(); intent.setClass(FoodList.this,Details.class); Bundle bundle = new Bundle(); String s[] = new String [5]; s[0] = data2.get(position).getName(); s[1] = data2.get(position).getMaterial(); s[2] = data2.get(position).getType(); s[3] = data2.get(position).getContent(); s[4] = data2.get(position).getIs_star()?"yes":"no"; bundle.putStringArray("msg",s); intent.putExtras(bundle); startActivityForResult(intent,REQUEST_CODE);//REQUEST_CODE --> 1 }
    然后,在Detail.java中返回参数到主页面的函数。当点击星星以及收藏时候,我们只改变MyCollection的属性,而不是真正返回活动,而到点击返回按钮时候才根据这些改变的属性来传递不同的参数。

    当返回2时,表示详情页面出现了收藏事件,必须将MyCollection的信息传递回去bundle.putSerializable(“collect”, temp),并且使用setResult来返回参数
    当返回3时,表示详情页面出现了改变星星状态事件
    当返回4时,表示两种事件同时发生

    不然,则直接调用finish事件来结束活动。
    //处理返回按钮 final ImageView back_but = findViewById(R.id.back); back_but.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { if(temp.getIs_collected() == true && temp.getIs_star() != (str[4].equals("yes"))){ Intent intent = new Intent(Details.this, FoodList.class); Bundle bundle = new Bundle(); bundle.putSerializable("collect", temp); intent.putExtras(bundle); setResult(4,intent); //RESULT_CODE --> 4 } //收藏夹 else if (temp.getIs_collected() == true) { Intent intent = new Intent(Details.this, FoodList.class); Bundle bundle = new Bundle(); bundle.putSerializable("collect", temp); intent.putExtras(bundle); setResult(2,intent); //RESULT_CODE --> 2 } //保存星星状态 else if(temp.getIs_star() != (str[4].equals("yes"))){ Intent intent = new Intent(Details.this, FoodList.class); Bundle bundle = new Bundle(); bundle.putSerializable("collect", temp); intent.putExtras(bundle); setResult(3,intent); //RESULT_CODE --> 3 } Details.this.finish(); } });
    这样在主页面中只需重构OnActivityResult函数即可以处理这些事件。处理结果为2时,从intent中拿回食物的信息,通知收藏夹列表来改变列表,这里使用我的私有函数refreshList,太过简单也不再细述,详情参见代码。
    而处理结果为3时,则要在两个列表中查找所有该食物的状态,更改星星的情况,以此实现星星状态的长期保存。
    // 为了获取结果 @Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { super.onActivityResult(requestCode, resultCode, data); if (resultCode == 2) { if (requestCode == REQUEST_CODE) { MyCollection mc = (MyCollection)data.getSerializableExtra("collect"); refreshList(mc,simpleAdapter); } } else if(resultCode == 3){ if (requestCode == REQUEST_CODE) { MyCollection mc = (MyCollection)data.getSerializableExtra("collect"); for(int i = 0; i < data2.size(); i++){ if(data2.get(i).getName().equals(mc.getName())){ data2.set(i,mc); } } for(int i = 0; i < favourite.size(); i++){ if(favourite.get(i).get("recipeName").toString().equals(mc.getName())){ favourite.get(i).remove("star"); favourite.get(i).put("star",mc.getIs_star()); } } } } else if(resultCode == 4){ if (requestCode == REQUEST_CODE) { MyCollection mc = (MyCollection)data.getSerializableExtra("collect"); for(int i = 0; i < data2.size(); i++){ if(data2.get(i).getName().equals(mc.getName())){ data2.set(i,mc); } } refreshList(mc,simpleAdapter); } } }
    7.在Detail页面来根据不同的食物内容来动态生成UI界面这里的动态包括,详情页面上部的颜色,以及名字,营养成分等等。星星的状态也要动态生成。
    通过intent拿出的信息来改变xml中相对应id的组件,只需注意id的正确,以及颜色的选取即可。
    Bundle bundle=this.getIntent().getExtras(); str = bundle.getStringArray("msg"); TextView name = findViewById(R.id.name); name.setText(str[0]); TextView material = findViewById(R.id.material); material.setText("富含 "+ str[1]); TextView type = findViewById(R.id.type); type.setText(str[2]); temp = new MyCollection(str[0],str[3],str[2],str[1],false); //根据上次情况保存星星状态 final ImageView star_but = findViewById(R.id.star); if(str[4].equals("yes")){ star_but.setImageResource(R.mipmap.full_star); temp.setIs_star(true); } else{ star_but.setImageResource(R.mipmap.empty_star); temp.setIs_star(false); }
    3.2.3 实验遇到的困难以及解决思路1.RecycleView无法正确生成列表按照老师给的教程一步步写好Adapter与Holder后,运行应用时出现闪退情况。报错信息为,无法得到资源。一有报错,第一步当然是将报错信息扔上搜索引擎,但是网页上的信息都说是因为setText()里面的参数为String而不是其他。但细看自己的程序并没有出现setText的错误参数情况。
    然后,我对于类的传参开始找问题,结果发现是convert函数在传参的时候,没有找到资源,而是一个空的对象。于是再修改convert函数后,完成了这一部分的工作。
    2.收藏列表的错误点击收藏列表的第一项为“*”与“收藏夹”,这两个不应该被触发点击事件,否则会传递一个空的MyCollection到详情页面会出现报错。所以必须在点击收藏列表的监听函数时加一个判断,当点击的是第一个item时,不要触发跳转事件。
    3.食物详情页面的UI设计不符合位置这次详情页面的UI有点难度,对于三分之一的上部设置就弄了相当长的时间,当知道使用layout_weight时候,然而在实际使用的时候,却并没有达到三分之一的效果。后来,才知道没有将height设置为0dp,而是为wrap_content.。这样导致权重设置失败。
    其次,对于设置分割线以及收藏图标如何垂直居中,间距合适遇到了困难。由于我在下部使用的是ConstrainLayout布局,所以必须要以别的组件来作相对设置位置。这里我对于这两个组件,分别相对于parent的上方,以及下面分割线的下方作为限制。这样就好像上下两个作用力,使其位于垂直居中的位置。
    最后只需调整线条的长度以及图片的大小即可。
    <TextView android:id="@+id/ver_line" android:layout_width="2dp" android:layout_height="45dp" android:background="#1E000000" app:layout_constraintTop_toTopOf="parent" app:layout_constraintBottom_toBottomOf="@id/line1" android:gravity="center" android:layout_marginRight="10dp" app:layout_constraintRight_toLeftOf="@+id/collect" /> <ImageView android:id="@+id/collect" android:layout_width="45dp" android:layout_height="45dp" android:scaleType="fitXY" android:src="@mipmap/collect" app:layout_constraintBottom_toBottomOf="@id/line1" app:layout_constraintTop_toTopOf="parent" app:layout_constraintRight_toRightOf="parent" android:layout_marginRight="20dp" />
    4.Detail页面与Foodlist页面进行交换信息时候,对于数据包的处理由于要使用intent来实现不同活动中的交互,必须将食物的信息传递到详情页面,以及在详情页面中改变后的食物信息传递回食物列表存储。于是,就要求他们交换信息时候必须要满足两个条件,第一是要一次传递一个食物对象,第二是要满足intent的信息交互函数。这里使用的是bundle的putSerializable函数,这也要求我们的食物类必须要实现Serializable类。
    Intent intent = new Intent(Details.this, FoodList.class); Bundle bundle = new Bundle(); bundle.putSerializable("collect", temp); intent.putExtras(bundle); setResult(2,intent); //RESULT_CODE --> 2
    四、实验思考及感想4.1 UI界面设计与基础事件交互本次是第一次安卓开发的实验,主要关于UI界面的设计与一些简单的交互,这与我之前学过的web网页设计十分相似,定义组件以及通过id来对于每一个组件来设置一些监听函数,完成所需要的功能。
    但是,安卓开发上也有许多不同之处,对于java文件中必须要了解调用组件的监听函数,名字比较长,而且参数多,必须在平时熟练使用并要经常查阅手册。
    对于ui界面,我这次主要是通过xml的书写来生成界面,用里面的一些属性来定义组件的大小,边距等等,除此之外,安卓开发中还很讲究文件的分类,将string,color,style设置成另外的文件,在主的xml可以调用这些文件中的内容来实现,这样的好处便于修改界面的内容,例如可以根据这个来开发中文英文不同的ui界面版本。
    4.2 Intent、Bundle的使用以及RecyclerView、ListView的应用这次实验花了不少的时间来理解不同列表的实现方式,学习了不同ui布局的位置设置,活动之间的交互信息方法,按钮监听函数。
    但是,对于实验基本要求所做出了的应用程序还是有一些不太完美的地方,于是,我做了一些改进的地方(加分项),使其更加符合日常使用,包括对于详情列表星星的状态保存,在详情页面不按返回图标而是点击手机的返回键时无法收藏该食物状态,还为RecycleList加了一个自定义的动画效果,使其更加美观。
    4.2.1 对于星星状态的持久化改进星星的状态持久化,我实现出来的效果是当该食物被加星后,无论是在食物列表还是在收藏列表都会出现加星的同步状态,不会出现个别加星个别不加星。
    这里实现的持久化,实际就是给食物添加多一个is_star属性来判断该食物的状态,并将该状态传递到详情页面来动态处理。
    //根据上次情况保存星星状态 final ImageView star_but = findViewById(R.id.star); if(str[4].equals("yes")){ star_but.setImageResource(R.mipmap.full_star); temp.setIs_star(true); } else{ star_but.setImageResource(R.mipmap.empty_star); temp.setIs_star(false); }
    而在改变后返回到其他界面时,也要将改变了的星星状态返回,以此改变该食物在数据结构中的信息
    else if(resultCode == 3){ if (requestCode == REQUEST_CODE) { MyCollection mc = (MyCollection)data.getSerializableExtra("collect"); for(int i = 0; i < data2.size(); i++){ //更新FoodList if(data2.get(i).getName().equals(mc.getName())){ data2.set(i,mc); } } for(int i = 0; i < favourite.size(); i++){ //更新CollectList if(favourite.get(i).get("recipeName").toString().equals(mc.getName())){ favourite.get(i).remove("star"); favourite.get(i).put("star",mc.getIs_star()); } } } }
    4.2.2 对于手机系统返回键的处理这里出现的bug是在详情页面点击收藏后,不按返回小图标,而是点击手机返回键时,无法收藏该食物。这是因为点击手机收藏键是没有将信息传递回主页面的,所以我们必须根据这个按键重构返回键的功能,来让该功能与点击返回小图标是一样的。
    当在详情页面,得到返回键被单击时,实现的功能与点击返回图标相同。而其他则继续执行系统的默认按键功能在最后添加return super.onKeyDown(keyCode, event);
    //点击返回时候,加入收藏也要生效 @Override public boolean onKeyDown(int keyCode, KeyEvent event) { if (keyCode == KeyEvent.KEYCODE_BACK) { //两种情况同时实现 if(temp.getIs_collected() == true && temp.getIs_star() != (str[4].equals("yes"))){ Intent intent = new Intent(Details.this, FoodList.class); Bundle bundle = new Bundle(); bundle.putSerializable("collect", temp); intent.putExtras(bundle); setResult(4,intent); //RESULT_CODE --> 4 } //收藏夹 else if (temp.getIs_collected() == true) { Intent intent = new Intent(Details.this, FoodList.class); Bundle bundle = new Bundle(); bundle.putSerializable("collect", temp); intent.putExtras(bundle); setResult(2,intent); //RESULT_CODE --> 2 } //保存星星状态 else if(temp.getIs_star() != (str[4].equals("yes"))){ Intent intent = new Intent(Details.this, FoodList.class); Bundle bundle = new Bundle(); bundle.putSerializable("collect", temp); intent.putExtras(bundle); setResult(3,intent); //RESULT_CODE --> 3 } else{ Details.this.finish(); } } return super.onKeyDown(keyCode, event); }
    4.2.3 实现RecycleList的动画在res文件夹建立anim文件夹来放置动画的xml文件,首先要建立layout_animation_fall_down.xml文件。
    其中animation为列表每一项item的动画,其文件在后面再实现,delay表示动画的延迟时间,animationOrder表示动画item的顺序是正常,即从大到小,在这里实现的效果就是从高到低。
    <?xml version="1.0" encoding="utf-8"?><layoutAnimation xmlns:android="http://schemas.android.com/apk/res/android" android:animation="@anim/item_animation_fall_down" android:delay="15%" android:animationOrder="normal" />
    接着对item实现layout_animation_fall_down.xml文件,来控制列表每一项的动画效果。
    translate组件中fromYDelta表示item首先位于y轴的上方20%出发,然后toYDelta表示item所要到达的位置,这里的0表示为回到本应该的位置。interpolator里面的属性表示减速实现动画过程。
    alpha组件表示透明度的变化,由0到1,加速实现动画过程。
    scale组件表示item的大小,由105%变化为100%,略微缩放动画。
    <set xmlns:android="http://schemas.android.com/apk/res/android" android:duration="500" > <translate android:fromYDelta="-20%" android:toYDelta="0" android:interpolator="@android:anim/decelerate_interpolator" /> <alpha android:fromAlpha="0" android:toAlpha="1" android:interpolator="@android:anim/decelerate_interpolator" /> <scale android:fromXScale="105%" android:fromYScale="105%" android:toXScale="100%" android:toYScale="100%" android:pivotX="50%" android:pivotY="50%" android:interpolator="@android:anim/decelerate_interpolator" /></set>
    这样就实现了一个列表的从上到下,逐渐出现的动画。
    4.3 感想通过不断的学习,总算理解了android的一些机制,也能简单的写出了一个程序了。但是对于java语言的虚函数,静态函数,接口,数据类型等等都需要加强,这会使我更方便地理解类与类之间的关系。对于ui的设计要熟练掌握三种布局的运用,可以适当给某些组件先赋值通过preview来查看位置,再在java文件中实现动态赋值,这样做既能保证ui也能动态生成页面。这次实验使用的是绑定数据是运用数组,猜想未来应该可以引入数据库的绑定,这样会使代码更加简洁。
    1 留言 2019-07-11 12:12:51 奖励20点积分
  • 真实感图形学研究

    一、介绍真实感图形学是计算机图形的核心内容之一,是最能直接反映图形学魅力的分支。
    寻求能准确地描述客观世界中各种现象与景观的数学模型,并逼真地再现这些现象与景观,是图形学的一个重要研究课题。很多自然景物难以用几何模型描述,如烟雾、植物、水波、火焰等。本文所讨论的几种建模及绘制技术都超越了几何模型的限制,能够用简单的模型描述复杂的自然景物。
    二、自然景物模拟  在计算机的图形设备上实现真实感图形必须完成的四个基本任务。

    三维场景的描述。三维造型
    将三维几何描述转换成为二维透视图。透视变换
    确定场景中的所有可见面。消隐算法,可见面探测算法
    计算场景中可见面的颜色。根据基于光学物理的光照模型计算可见面投射到观察者眼中的光亮度大小和色彩组成

    其中三维造型技术根据造型对象分成三类:

    曲面造型:研究在计算机内如何描述一张曲面,如何对它的形状进行交互式的显示和控制。曲面造型又分成规则曲面造型(如平面、圆柱面等)和不规则曲面两种。不规则曲面造型方法主要有Bezier曲线曲面、B样条曲线曲面和孔斯曲面等
    立体造型。研究如何在计算机内定义、表示一个三维物体。这些方法主要有体素构造法、边界表示法、八叉树法等等。曲面造型和立体造型合称为几何模型造型
    自然景物模拟。研究如何在计算机内模拟自然景物,如云、水流、树等等。本文将主要集中介绍有 关自然景物模拟的有关方法

    寻求能准确地描述客观世界中各种现象与景观的数学模型,并逼真地再现这些现象与景观,是图形学的一个重要研究课题。很多自然景物难以用几何模型描述,如烟雾、植物、水波、火焰等。本文所讨论的几种建模及绘制技术都超越了几何模型的限制,能够用简单的模型描述复杂的自然景物。
    2.1 分形与IFS2.1.1 分形几何分形(fractal)指的是数学上的一类几何形体,在任意尺度上都具有复杂并且精细的结构。一般来说分形几何体都是自相似的,即图形的每一个局部都可以被看作是整体图形的一个缩小的复本。例如,雪花曲线是一种典型的分形图形,生成方法如下:取一等边三角形,在每一边中间的三分之一处分别生长出一个小的等边三角形,重复上述过程就可以形成图2.1所示的曲线。理论上来说,无限递归的结果是形成了一个有限的区域,而该区域的周长却是无限的,并且具有无限数量的顶点。这样的曲线在数学上是不可微的。
    早在19世纪就已经出现了一些据有自相似特性的分形图形,但最初只是被看作一种奇异现象。本世纪70年代,Benoit B. Mandelbrot最早对分形进行系统研究,并创立了分形几何这一新的数学分支。Mandelbrot扩展了经典欧几里得几何中的维数,提出了分数维的概念。 分形几何并不只是抽象的数学理论。例如海岸线的轮廓,如果考虑其不规则性,同样具有无限的长度。Mandelbrot认为海岸、山脉、云彩和其他很多自然现象都具有分形的特性。因此,分形几何已经成为一个发展十分迅速的科学分支,尤其是在计算机图形学中,成为描述自然景物及计算机艺术创作的一种重要手段。此外,分形在图象压缩方面也有广阔的应用前景。
    2.1.2 仿射变换与迭代函数迭代函数系统IFS (Iteration Function System)最早是由Hutchinson于1981年提出的,现已成为分形几何中的重要研究内容之一。IFS是以仿射变换为框架,根据几何对象的整体与局部具有自相似结构,经过迭代而产生的。  、
    2.1.3 基于分形的景物生成由IFS码绘出的分形图形具有无穷细微的自相似结构,能对很多客观事物作出准确的反映,这种结构是难于用经典数学模型来描述的。只要变换选取适当,利用IFS就可以迭代地生成任意精度的图形效果,这也是其他绘制方法难以做到的。
    2.2 基于文法的模型美国科学家Aristid Lindenmayer于1969年提出了一种研究植物形态与生长的描述方法,以他的名字命名为L系统(L-grammars)。1984年,A. R. Smith将L系统应用于计算机图形学中。L系统实际上是一组形式语言,由特定的语法加以描述,这些语法由一系列产生式组成,所有产生式都是直接匹配的。例如,一种典型的L系统语法包括四个字母{A,B,[,]}和两条产生式规则:
    A→AAB→A[B]AA[B]从字母A出发,可以迭代地生成A、AA、AAAA等字母序列;从字母B出发,前几步迭代结果如下:
    BA[B]AA[B]AA[A[B]AA[B]]AAAA[A[B]AA[B]]……如果我们把由这种语法规则中的产生式迭代形成的词汇看作是某种图结构的一部分,把方括号中的内容视为前一个符号的分支,则上述文法的三次迭代结果如图2.2所示。在此基础上,适当改变分支的方向,加入随机动因素及在分支的终点绘制出叶子、花、果实等细节,就可以逼地真模拟出现实世界中各种形态的植物。
    当然,上述L系统本身并没有记录任何几何信息,因此基于L系统的建模语言必须能够同时支持文法描述和几何描述;如何对L系统的生长(迭代)过程加以控制也是一个需要进行研究的问题。对此,Reffye、Prusinkiewicz等人分别提出了各自的方法。
    总之,基于文法的L系统用于植物生长过程的模拟是非常成功的,为计算机真实感图形的绘制提供了又一个有力的工具。此外,这种思想也被成功地应用到了电子线路设计和建筑设计等很多方面。
    2.3 粒子系统Reeves于1983年提出的粒子系统方法是一种很有影响的模拟不规则物体的方法,能够成功地模拟由不规则模糊物体组成的景物。与其他传统图形学方法完全不同,这种方法充分体现了不规则模糊物体的动态性和随机性,从而能够很好地模拟火、云、水、森林和原野等许多自然景象。
    粒子系统的基本思想是采用许多形状简单的微小粒子作为基本元素来表示不规则模糊物体。这些粒子都有各自的生命周期,在系统中都要经历”产生”、”运动和生长”及”消亡”三个阶段。粒子系统是一个有”生命”的系统,因此不象传统方法那样只能生成瞬时静态的景物画面,而可产生一系列运动进化的画面,这使得模拟动态的自然景物成为可能。生成系统某瞬间画面的基本步骤是:

    产生新的粒子
    赋予每一新粒子一定的属性
    删去那些已经超过生存期的粒子
    根据粒子的动态属性对粒子进行移动和变换
    显示由有生命的粒子组成的图象

    粒子系统采用随机过程来控制粒子的产生数量,确定新产生粒子的一些初始随机属性,如初始运动方向、初始大小、初始颜色、初始透明度、初始形状以及生存期等,并在粒子的运动和生长过程中随机地改变这些属性。粒子系统的随机性使模拟不规则模糊物体变得十分简便。
    三、消隐及真实感图形生成  3.1 消隐在计算机图形学中,有三种方式表示三维物体:线框图、消隐图和真实感图。其中真实感图形的生成也要在消隐基础上进行光照处理。所谓消隐就是给定一组三维对象及投影方式(视见约束),判定线、面或体的可见性的过程。根据消隐在间的不同,消隐算法可分为两类:

    物体空间的消隐算法,消隐在规范化投影空间中进行,将物体表面的k个多边形中的每一个面与其余的k-1个面进行比较,精确地求出物体上每条棱边或每个面的遮挡关系。这类算法的计算量正比于k2
    图象空间的消隐算法,消隐在屏幕坐标系中进行,对屏幕上的每一个象素进行判断,确定在该象素点上可见的面。若屏幕分辨率为m×n,物体空间中共有k个多边形,则此类算法的的计算量正比于mnk

    大多数消隐算法都涉及排序和相关性的概念。排序是为了确定消隐对象之间的遮挡关系,通常在X、Y、Z三个方向分别进行。消隐算法的效率在很大程度上取决于排序的效率。相关性是指物体对象或其变换后的图象局部保持不变的性质,在消隐算法中利用相关性是提高排序率的重要手段。
    常用的物体空间消隐算法有多边形区域排序算法和列表优先算法等。
    Z-Buffer (深度缓存)是最简单的图象空间面消隐算法,深度缓存数组的使用避免了复杂的排序过程在分辨率一定的情况下,算法计算量只与多边形个数成正比。该算法也便于硬件实现和并行处理。在此基础上,Z-Buffer扫描线算法利用了多边形边和象素点的相关性,使得算法效率进一步提高。扫描线算法也为简单光照模型提供了良好的消隐基础。
    3.2 简单光照模型及明暗处理光照模型(Illumination Model)是根据有关光学定律,计算真实感图形中各点投射到观察者眼中的光线强度和色彩的数学模型。简单的局部光照模型假定光源是点光源,物体是非透明体,不考虑折射,反射光由环境光、漫反射光和镜面反射光组成。  基于局部光照模型及明暗处理的阴影生成算法也有很多。阴影是指景物中哪些没有被光源直接照射到的按区。在计算机生成的真实感图形中,阴影可以反映画面中景物的相对位置,增加图形的立体感和场景的层次感,丰富画面的真实感效果。阴影可分为本影和半影两种。本影加上它周围的半影组成软影区。单个点光源照明只能形成本影,多个点光源和线光源才能形成半影。
    对多边形表示的物体,一种计算本影的方法是影域多边形方法,环境中物体的影域定义为视域多面体和光源在景物空间中被物体轮廓多边形遮挡的区域的交集。这种方法的实现可以利用现有的扫描线消隐算法。Athherton等人提出了曲面细节多边形方法,以多边形区域分类的隐藏面消去算法为基础,通过从光源和视点两次消隐生成阴影。
    以上两种阴影生成方法只适用于用多边形表示的景物,无法解决光滑曲面片上的阴影生成问题。为此Williams提出了Z-Buffer方法,首先利用Z-Buffer算法按光源方向对景物进行消隐,然后再用Z-Buffer算法按视线方向进行会制。这种方法可以方便地在理包括光滑曲面的任意复杂的景物,但存储量大,阴影区域附近易产生走样。
    3.3 整体光照模型与光线跟踪照射到物体上的光线,不仅有从光源直接射来的,也有经过其它物体反射或折射来的。局部光照模型只能处理直接光照,为了对环境中物体之间的各种反射、折射光进行精确模拟,需要使用整体光照模型。
    相对于局部光照模型,整体光照模型可以表示为Iglobal=KRIR+ KTIT。其中Iglobal为非直接光照对物体上一点光强的贡献;IR为其他物体从视线的反射方向R反射或折射来的光强,KR为反射系数;KT为其 他物体从视线的折射方向T折射或反射来的光强,IT为折射系数。将Iglobal与局部光照模型的计算结果相叠加,就可以得到物体上点的光强。
    光线跟踪算法是典型的整体光照模型,最早由Goldste、Nagel和Appel等人提出,Appel用光线跟踪的方法计算阴影;Whited和Kay扩展了这一算法,用于解决镜面反射和折射问题。算法的基本思想如下:
    对于屏幕上的每个象素,跟踪一条从视点出发经过该象素的光线,求出与环境中物体的交点。在交点处光线分为两支,分别沿镜面反射方向和透明体的折射方向进行跟踪,形成一个递归的跟踪过程。光线每经过一次反射或折射,由物体材质决定的反射、折射系数都会使其强度衰减,当该光线对原象素光亮度的。贡献小于给定的阈值时,跟踪过程即停止。光线跟踪的阴影处理也很简单,只需从光线与物体的交点处向 光源发出一条测试光线,就可以确定是否有其他物体遮挡了该光源(对于透明的遮挡物体需进一步处理光强的衰减),从而模拟出软影和透明体阴影的效果。
    光线跟踪很自然地解决了环境中所有物体之间的消隐、阴影、镜面反射和折射等问题,能够生成十分逼真的图形,而且算法的实现也相对简单。但是,作为一种递归算法其计算量十分巨大。尽量减小求交计 算量是提高光线跟踪效率的关键,常用的方法有:包围盒(entents)、层次结构(hierarchies)及区域分割 (spatial partitioning)等技术。
    光线跟踪是一个典型的采样过程,各个屏幕象素的亮度都是分别计算的,因而会产生走样,而算法本身的计算量使得传统的加大采样频率的反走样技术难以实用。
    象素细分是一种适用于光线跟踪的反走样技术,具体方法是: 首先对每一象素的角点用光线跟踪计算亮度;然后比较各角点的亮度,若差异较大,则将象素细分为4个子区域,并对新增的5个角点用光线跟踪计算亮度;重复比较与细分,直到子区域各角点亮度差异小于给定的阀值为止;最后加权平均求出象素点的显示亮度。
    与象素细分不同,Cook、Porter和Carpenter 提出的分布式光线跟踪是一种随机采样的方法,在交点处镜面反射方向和折射方向所夹的立体角内,按照一定的分布函数同时跟踪若干根光线,然后进行加权平均。Cook等人还提出了利用分布式随机采样技术模拟半影、景深和运动模糊等效果的方法。
    光线跟踪的另一个问题是,光线都是从视点发出的,阴影测试光线则需另外处理,因而无法处理间接的反射或折射光源,例如镜子或透镜对光源所产生的作用就难以模拟。为解决这一问题,可以从光源和视点出发对光线进行双向跟踪。但是,大量从光源出发的光线根本不可能到达屏幕,这使得双向光线跟踪的计算量显著增大,难以实用。Heckbert和Hanrahanr提出的解决方法是只将从光源出发的光线跟踪作为常归光线跟踪的补充;Arvo方法则是对从光源发出进入环境的光线进行预处理;邵敏之和彭群生等人也提出 了基于空间线性八叉树结构的对光源所发出光线进行优化的双向光线跟踪算法。
    3.4 漫反射和辐射度方法常规光照模型假设物体间的漫反射是一个恒定的环境光,即使双向光线跟踪也只能处理物体间的反射与折射,而不能处理物体间的漫反射。最初由Goral等人于1984年及Nishita等人于1985年提出的辐射度方法是一种基于热能工程的方法,用光辐射的产生和反射代替环境光,从而能够精确处理对象之间的光反射问题。
    辐射度方法将景物和光源视为一个封闭的系统,在系统中光能量是守衡的;并假定构成景物的曲面都是理想的漫反射面。所谓辐射度,是指单位时间内从曲面上单位面积反射出去的光能量,记为B。在理想情况下,可以近似认为逼近曲面的面片上的光强是均匀的,即漫反射各向均匀。根据能量守衡定律
    辐射度方法的主要计算量在于计算形状因子。Cohen和Greenberg提出的半立方体方法是一种近似计算 封闭面形状因子的高效方法。首先以面片i的中心为原点,法向量为Z轴建立一个半立方体,将其五个表面划分成均匀的网格,每个网格单元的微形状因子可以预先求得;然后将场景中所有其他面片都投影到半立方体上,对于多个面片投影到同一个网格单元的情况需在投影方向上进行深度比较,网格单元只保留最近的面片,这一过程相当于Z-Buffer算法;最后将半立方体中所有与面片j相关的网格单元的微形状因子累加,即可得到面片i相对于面片j的形状因子Fij。 辐射度方法的优点在于算法与视点无关、计算和绘制可以分别独立进行、能够模拟颜色渗透效果等,但无法处理镜面反射与折射。
    在辐射度方法中,面片向特定方向辐射出的光能量仅总辐射度有关,而与所接受能量的方向无关。Immel、Cohen和Greenberg推广了这一方法,每个面片不只计算唯一的辐射度,而是将面片半球空间分割成有限个空间立体角的区域,在每个区域内分别计算输入输出的光能量,通过双向辐射函数计算向某一方向辐射能量的概率,每个顶点的光强可以通过对与视点方向最为接近的若干方向上的辐射度进行插值得到,并最终完成图形生成。这种改进方法可以处理包含镜面和透明物体的复杂场景,但要付出巨大的时间开销和空间开销。
    另一种方案是将辐射度与光线跟踪相结合,仅仅将两种方法的计算结果相加是不够的,必须同时处理漫反射面和镜面之间的光照关系。Wallace、Cohen和Greenberg提出了一种两步方法:

    第一步执行与视点无关的辐射度方法,辐射度的计算必须考虑镜面,这可以通过镜象法(mirror-world approach)予以模拟
    第二步执行基于视点的光线跟踪算法,处理整体镜面反射和折射,并生成图形。算法效率的关键在于第一步,其中镜象法只需处理理想镜面的反射作用,并据此对形状因子加以修正,形状因子的计算量将随镜面数量的增加而显著增加

    Sillon和Puech进一步扩展了上述两步法,在第一步时不采用镜象法,而是用递归的光线跟踪来计算形状因子,可以处理具有任意数量镜面及透明体的场景。
    3.5 纹理映射纹理映射(Texture Mapping)是通过将数字化的纹理图象覆盖或投射到物体表面,从而为物体表面增加表面细节的过程。纹理图象可以通过采样得到,也可以通过数学函数生成。物体的很多表面细节多边形逼近或其他几何建模的方法是难以表现的,因此纹理映射技术能够使得计算机生成的物体看起来更加逼真自然。
    纹理映射技术最早由Catmull提出,经Blinn和Newell改进后得到广泛应用,成为计算机图形学中的一种重要方法。将纹理映射到物体表面,可以看作是将一个屏幕象素投影到纹理空间的对应区域并计算该区域的平均颜色,以求得真正象素颜色的最佳近似值。具体地说,纹理图象存在于独立的纹理空间中,映射分为两步进行,先将屏幕象素通过四个角点坐标映射到三维物体表面,再进一步映射到纹理空间,形成一个四边形区域,即对屏幕象素映射到三维物体表面上所形成的复杂曲面片的近似。屏幕象素的纹理映射结果可以通过对纹理空间中四边形区域进行累加得到。也可以采用相反的映射方式,即从纹理空间到三维物 体再到屏幕象素进行映射,但这种映射方式需要占用更大的存储空间,更易产生走样,并且无法应用于扫描线算法。
    物体表面的纹理可分为两类:颜色纹理和几何纹理。颜色纹理主要是指同一表面各处呈现出不现的花纹和颜色;几何纹理主要指物体表面在微观上呈现出的起伏不平。上述纹理映射方法只能处理颜色纹理,所生成的物体表面仍然是光滑的。Blinn在纹理映射基础上提出的Bump Mapping方法是一种模拟物体表面粗糙纹理的技术,可以不用对物体的粗糙表面在几何上进行建模就可以改善物体表面的微观结构,如大理石纹理表面雕刻的文字、混凝土墙面等效果。此外,更高级的真实感图形效果如人脸上流淌的汗水也可以通过随时间变化的Bump mapping来模拟。Bump Map是一个二维数组,数组中每个元素是物体表面上一点比 其实际位置略高或略低的偏移向量。这些微小偏移被映射到物体表面一点后修正该点处的法向量,通过修正后的法向量进行光照计算。
    纹理图象和屏幕象素都是离散的采样系统,很容易产生走样,即丢失纹理细节,使表面边界产生锯齿。纹理映射中常用的反走样方法是卷积滤波法。屏幕象素是一个矩形区域,映射到纹理空间上为一任意四 边形,卷积滤波法就是取四边形所覆盖区域的纹理函数的卷积作为屏幕象素的光亮度,可以采用盒形、三角形、高斯分布及样条函数等作为滤波函数。在实际应用中为简化计算,常用正方形、矩形或椭圆等形状近似表示屏幕象素所覆盖的任意四边形区域。卷积滤波法是非线性的,计算量较大,并且不适用于Bump Mapping,因为Bump Mapping的纹理函数与象素的光亮度之间不是线性关系,此时可以使用前置滤波法。前置滤波是在纹理空间中按照不同的分辨率将一定区域内的纹理平均值预先算好,执行映射时只需按照屏幕象素所覆盖的区域大小选取一定的分辨率查表,并作适当线性插值即可。
    以上二维映射在很多情况下都能得到很好的效果,但有时会产生失真,如在三维曲面上仍会呈现出二维效果,以及产生纹理接缝问题等。Peachey和Perlin提出了一种基于实体纹理的方法,用物体上点在三维空间的位置的函数作为纹理,从而更精确地表现木材或大理石等的雕刻效果。
    其他一些材质的表面也可以用适当的方法模拟,如Gardner的透明映射方法可以用简单的形状模拟云彩。此外,很多基于物理模型、随机过程和分形几何等的方法也被用来生成自然纹理。
    1 留言 2019-06-27 11:19:59 奖励16点积分
  • 机器学习 24 、MF ANN

    前文链接:https://write-bug.com/article/2696.html
    MF(Matrix Factorization)基于矩阵分解的推荐算法-隐语义模型:ALS、LFM、SVD、SVD++
    在15年左右流行
    ALS:-交替最小二乘
    我们之前学习的协同过滤CF:单独考虑user、item
    这里同时考虑User-item两方面:
    原来我们的UI稀疏打分矩阵\<m,n>:

    一般公司用户量可以随意达到上亿,物品量如音乐可以达到几十万,用户量之所以多,是因为可能一个用户有多个账户等等,共同组成一个很大很稀疏的矩阵那么面对这样一个矩阵,我们可以通过矩阵分解来解决:
    将User和Item分别映射到新的空间,分解成两个矩阵,U 和I两个维度都不变



    K值远小于M和N,从而达到降维目的
    无需关注新空间的各个维度,只需假定存在 ,即用向量表示user和item
    新的维度称为Latent Factor(隐因子)

    K的维度相比原来来说很小很小,并且可以人为设定,只需要两个矩阵相乘就能得到UI矩阵,即R’ ≈R
    两个矩阵相似如何界定?误差
    误差表示:RMSE :均方根误差
    目标:真实矩阵,和结果矩阵之间的尽可能逼近
    最小化平方误差作为损失函数:

    考虑矩阵稳定性,引入L2正则项,得到损失函数:

    这里的rui就是上面所说的R,即原矩阵user对item打的分数
    xuyi即分解矩阵后再相乘的预估分数,两个相减就是误差
    未知数:
    Xu:user vectoryi:item vector如何求解最优值:求导=0
    公式1:

    导数为0,可得到:

    同理对yi求导(公式2):

    为什么叫交替二乘法?
    这里的做法是让x和y交替下降:

    随机生成X、Y向量(初始化)
    固定Y,更新X(公式1)
    固定X,更新Y(公式2)
    第2、3步循环,直到满足收敛条件(RMSE)

    ——均方根误差
    那么我们得到这两个小矩阵,可以做什么?
    item-item :IK*KI=IIUser-User:UK*KU=UUuser与item的向量
    LFM思路和ALS算法类似,区别在于,ALS利用坐标下降法,LFM利用梯度下降法
    假设:
    评分矩阵𝑅𝑚,𝑛,m个用户对n个物品评分

    𝑟𝑢,𝑖:用户u对物品i的评分
    𝑅𝑚,𝑛 = 𝑃𝑚,𝐹 ∙ 𝑄𝐹,𝑛:R是两个矩阵的乘积
    P:每一行代表一个用户对各隐因子的喜欢程度 ,即前面的user矩阵
    Q:每一列代表一个物品在各个隐因子上的概率分布,即前面的item矩阵


    在之前的随机梯度下降中,我们更新w(t+1)=w(t) - a*g(梯度)
    那么根据这样的思路,这里把矩阵P与Q的分数当作权重w来更新:

    矩阵展开后相当于对每个元素分数进行偏导:

    随意抽取中间这个元素作为代表,求取偏导:

    由于

    所以后面两个等式相等,之后就有一件很有意思的事情发生了:P的分数是由上一时刻的Q更新的,Q的分数是由上一时刻的P更新的。也就是类似上面的坐标下降法:交替进行。
    代入原式:

    LFM实践:
    class LFM(object): def __init__(self, rating_data, F, alpha=0.1, lmbd=0.1, max_iter=500): '''rating_data是list<(user,list<(position,rate)>)>类型 ''' self.F = F#维度K self.P = dict() self.Q = dict() self.alpha = alpha#学习率 self.lmbd = lmbd self.max_iter = max_iter#迭代轮数 self.rating_data = rating_data#矩阵 '''随机初始化矩阵P和Q''' for user, rates in self.rating_data: self.P[user] = [random.random() / math.sqrt(self.F) for x in xrange(self.F)] for item, _ in rates: if item not in self.Q: self.Q[item] = [random.random() / math.sqrt(self.F) for x in xrange(self.F)] def train(self): '''随机梯度下降法训练参数P和Q ''' for step in xrange(self.max_iter): for user, rates in self.rating_data: for item, rui in rates: hat_rui = self.predict(user, item) err_ui = rui - hat_rui for f in xrange(self.F): self.P[user][f] += self.alpha * (err_ui * self.Q[item][f] - self.lmbd * self.P[user][f]) self.Q[item][f] += self.alpha * (err_ui * self.P[user][f] - self.lmbd * self.Q[item][f]) self.alpha *= 0.9 # 每次迭代步长要逐步缩小 def predict(self, user, item): '''预测用户user对物品item的评分 ''' return sum(self.P[user][f] * self.Q[item][f] for f in xrange(self.F))if __name__ == '__main__': '''用户有A B C,物品有a b c d,列表模拟矩阵:''' rating_data = list() rate_A = [('a', 1.0), ('b', 1.0)] rating_data.append(('A', rate_A)) rate_B = [('b', 1.0), ('c', 1.0)] rating_data.append(('B', rate_B)) rate_C = [('c', 1.0), ('d', 1.0)] rating_data.append(('C', rate_C)) lfm = LFM(rating_data, 2) lfm.train() for item in ['a', 'b', 'c', 'd']: print(item, lfm.predict('A', item)) # 预测计算用户A对各个物品的喜好程度SVDLFM有什么缺点?没有考虑客观的偏置,所以带偏置的LFM称为SVD
    什么是偏置,比如说有的人很极端给一些物品很高或者很低的分数,而有的人给每个物品都很平均的分数,还有包括地区等等都会影响对物品的看法,所以就有一个偏置存在。
    偏置:事件固有的,不受外界影响的属性。

    𝜇:训练集中所有评分的平均值
    𝑏𝑢:用户偏置,代表一个用户评分的平均值
    𝑏𝑖:物品偏置,代表一个物品被评分的平均值


    更新:

    SVD实践:
    class BiasLFM(object): def __init__(self, rating_data, F, alpha=0.1, lmbd=0.1, max_iter=500): '''rating_data是list<(user,list<(position,rate)>)>类型 ''' self.F = F self.P = dict() self.Q = dict() self.bu = dict() self.bi = dict() self.alpha = alpha self.lmbd = lmbd self.max_iter = max_iter self.rating_data = rating_data self.mu = 0.0 '''随机初始化矩阵P和Q''' cnt = 0 for user, rates in self.rating_data: self.P[user] = [random.random() / math.sqrt(self.F) for x in xrange(self.F)] self.bu[user] = 0#初始化bu cnt += len(rates) for item, rate in rates: self.mu += rate if item not in self.Q: self.Q[item] = [random.random() / math.sqrt(self.F) for x in xrange(self.F)] self.bi[item] = 0#初始化bi self.mu /= cnt#计算μ def train(self): '''随机梯度下降法训练参数P和Q ''' for step in xrange(self.max_iter): for user, rates in self.rating_data: for item, rui in rates: hat_rui = self.predict(user, item) err_ui = rui - hat_rui #更新两个b self.bu[user] += self.alpha * (err_ui - self.lmbd * self.bu[user]) self.bi[item] += self.alpha * (err_ui - self.lmbd * self.bi[item]) for f in xrange(self.F): self.P[user][f] += self.alpha * (err_ui * self.Q[item][f] - self.lmbd * self.P[user][f]) self.Q[item][f] += self.alpha * (err_ui * self.P[user][f] - self.lmbd * self.Q[item][f]) self.alpha *= 0.9 # 每次迭代步长要逐步缩小 def predict(self, user, item): '''预测用户user对物品item的评分,加偏置 ''' return sum(self.P[user][f] * self.Q[item][f] for f in xrange(self.F)) + self.bu[user] + self.bi[item] + self.muif __name__ == '__main__': '''用户有A B C,物品有a b c d''' rating_data = list() rate_A = [('a', 1.0), ('b', 1.0)] rating_data.append(('A', rate_A)) rate_B = [('b', 1.0), ('c', 1.0)] rating_data.append(('B', rate_B)) rate_C = [('c', 1.0), ('d', 1.0)] rating_data.append(('C', rate_C)) lfm = BiasLFM(rating_data, 2) lfm.train() for item in ['a', 'b', 'c', 'd']: print(item, lfm.predict('A', item)) # 计算用户A对各个物品的喜好程度SVD++任何用户只要对物品i有过评分,无论评分多少,已经在一定程度上反映了用户对各个隐因子的喜好 程度𝑦𝑖 = (𝑦𝑖1,𝑦𝑖2,…,𝑦𝑖𝐹),y是物品携带的属性,什么意思?比如说A买了3个item,B买了3个item,每个item背后有一系列feature vector,那么我们用A买的3个item背后的fea向量相加(实际计算是学习更新出来的,不一定是相加)代表一个虚拟的物品A,间接表达了这个用户的偏好程度,同理得到向量B,那么对于这个每个人背后的虚拟物品向量,就是y

    所以这个打分是在P上又增加了一个类似于偏置的东西,并做了归一化

    𝑁(𝑢):用户u评价过的物品集合
    𝑏𝑢:用户偏置,代表一个用户评分的平均值
    𝑏𝑖:物品偏置,代表一个物品被评分的平均值


    SVD++实践:
    class SVDPP(object): def __init__(self, rating_data, F, alpha=0.1, lmbd=0.1, max_iter=500): '''rating_data是list<(user,list<(position,rate)>)>类型 ''' self.F = F self.P = dict() self.Q = dict() self.Y = dict() self.bu = dict() self.bi = dict() self.alpha = alpha self.lmbd = lmbd self.max_iter = max_iter self.rating_data = rating_data self.mu = 0.0 '''随机初始化矩阵P、Q、Y''' cnt = 0 for user, rates in self.rating_data: self.P[user] = [random.random() / math.sqrt(self.F) for x in xrange(self.F)] self.bu[user] = 0 cnt += len(rates) for item, rate in rates: self.mu += rate if item not in self.Q: self.Q[item] = [random.random() / math.sqrt(self.F) for x in xrange(self.F)] if item not in self.Y:#比之前svd多加了一个y,初始化y self.Y[item] = [random.random() / math.sqrt(self.F) for x in xrange(self.F)] self.bi[item] = 0 self.mu /= cnt def train(self): '''随机梯度下降法训练参数P和Q ''' for step in xrange(self.max_iter): for user, rates in self.rating_data: z = [0.0 for f in xrange(self.F)] for item, _ in rates: for f in xrange(self.F): z[f] += self.Y[item][f]#用户观看过物品的评分集合加和,即虚拟物品向量,即∑Yjf ru = 1.0 / math.sqrt(1.0 * len(rates)) s = [0.0 for f in xrange(self.F)] for item, rui in rates: hat_rui = self.predict(user, item, rates) err_ui = rui - hat_rui self.bu[user] += self.alpha * (err_ui - self.lmbd * self.bu[user]) self.bi[item] += self.alpha * (err_ui - self.lmbd * self.bi[item]) for f in xrange(self.F): s[f] += self.Q[item][f] * err_ui#每个物品的信息和误差相乘的累加 self.P[user][f] += self.alpha * (err_ui * self.Q[item][f] - self.lmbd * self.P[user][f]) self.Q[item][f] += self.alpha * ( err_ui * (self.P[user][f] + z[f] * ru) - self.lmbd * self.Q[item][f]) for item, _ in rates: for f in xrange(self.F): #Y的更新 self.Y[item][f] += self.alpha * (s[f] * ru - self.lmbd * self.Y[item][f]) self.alpha *= 0.9 # 每次迭代步长要逐步缩小 def predict(self, user, item, ratedItems): '''预测用户user对物品item的评分 ''' z = [0.0 for f in xrange(self.F)] for ri, _ in ratedItems: for f in xrange(self.F): z[f] += self.Y[ri][f] return sum( (self.P[user][f] + z[f] / math.sqrt(1.0 * len(ratedItems))) * self.Q[item][f] for f in xrange(self.F)) + \ self.bu[user] + self.bi[item] + self.muif __name__ == '__main__': '''用户有A B C,物品有a b c d''' rating_data = list() rate_A = [('a', 1.0), ('b', 1.0)] rating_data.append(('A', rate_A)) rate_B = [('b', 1.0), ('c', 1.0)] rating_data.append(('B', rate_B)) rate_C = [('c', 1.0), ('d', 1.0)] rating_data.append(('C', rate_C)) lfm = SVDPP(rating_data, 2) lfm.train() for item in ['a', 'b', 'c', 'd']: print(item, lfm.predict('A', item, rate_A)) # 计算用户A对各个物品的喜好程度ANNANN 多维空间检索算法,不完全是算法,是更偏工程的一种方法,时下正在流行的简单方式,从图像检索演化而来,复杂一点的方式一般使用DNN可以达到目的每个用户、物品背后都有自己的向量映射在多为空间的点上,我们的目标就是把这些向量全部映射在一个空间内,求user最近的item点
    稀疏场景适用物品召回:cb倒排(token)、cf倒排(user)
    —召回能力有限
    鲜花和巧克力在情人节的情况下可以关联起来,但是通过cb不能召回,通过cf需要很多很多用户共点击或者共现才会关联
    那么这里如何计算和user距离近的点?之前我们使用cos或jaccard,但是我不能把所有的物品都遍历一遍,计算量过大,所以就引出了ANN的近邻检索annoy
    Annoy目标:建立一个数据结构,在较短的时间内找到任何查询点的最近点,在精度允许的条件下通过牺牲准 确率来换取比暴力搜索要快的多的搜索速度
    先圈出一部分集合,遍历集合取top
    如果推荐的结果不理想,不喜欢,不是这个方法的问题,而是embedding方式的问题
    那么这个集合如何圈呢?
    方案:随机选择两个点,然后根据这两个点之间的连线确定一个可以垂直等分线段的超平面,灰色是两点的连 线,黑色是超平面

    从而由里面的点集合就构成了叶子节点,最终形成整棵树
    建立二叉树结构用于表示空间分布,每一个节点表示一个子空间
    假设,如果两个点在空间彼此靠近,任何超平面不会将他们分开,如果我们搜索空间中的任意一点,和其距离 近的点为推荐候选,通过对二叉树的遍历,可以完成
    重复上页步骤,继续分割 ,过程很像Kmeans要求:每个子节点包含数据不超过K个(10):


    如果落在这个空间内的节点候选太少怎么办?或者本来两个点距离很近,但是被两个空间分割导致不能计算,那么我们就拓宽区域、多建几棵树,每次建立长得样子都不太一样。
    annoy只是多维空间检索中的一种方法,其他还有:

    KD-Tree (KNN-开始不随机,直接找到全局最优,速度慢,建树慢)
    局部敏感哈希LSH
    HNSW
    OPQ等等

    ANN实践:
    annoy安装:pip
    import sysfrom annoy import AnnoyIndexa = AnnoyIndex(3)#建立3颗树a.add_item(0, [1, 0, 0])#添加用户和item点a.add_item(1, [0, 1, 0])a.add_item(2, [0, 0, 1])a.build(-1)print(a.get_nns_by_item(0, 100))#与0这个用户最近的top100集合print(a.get_nns_by_vector([1.0, 0.5, 0.5], 100))#与这个item vector 最近的top100集合下面这个代码是什么意思呢?随着我们候选集合圈的缩小,我们的计算量也在缩小,那么我们目标是让这个候选集合的top和全局的top尽量一致,也就是说要在计算量和召回率之间做一个权衡,那么全局的top我们就只能通过暴力遍历来知道答案了。
    from __future__ import print_functionimport random, timefrom annoy import AnnoyIndextry: xrangeexcept NameError: # Python 3 compat xrange = rangen, f = 100000, 40#初始化10w个点,40颗树t = AnnoyIndex(f)for i in xrange(n): v = [] for z in xrange(f): v.append(random.gauss(0, 1))#高斯分布初始化点 t.add_item(i, v)#添加在树里t.build(2 * f)t.save('test.tree')#保存树limits = [10, 100, 1000, 10000]#圈的大小k = 10#10个候选prec_sum = {}prec_n = 100time_sum = {}for i in xrange(prec_n): j = random.randrange(0, n)#随机选一个点作为用户 #print(j) closest = set(t.get_nns_by_item(j, k, n))#求取与这个用户j最近的全局top10 #print(closest) for limit in limits: t0 = time.time() toplist = t.get_nns_by_item(j, k, limit)#圈内的top10 T = time.time() - t0 found = len(closest.intersection(toplist))#用全局与圈内的top取交集 hitrate = 1.0 * found / k#准确率 #print(hitrate) prec_sum[limit] = prec_sum.get(limit, 0.0) + hitrate time_sum[limit] = time_sum.get(limit, 0.0) + T print(prec_sum[limit])for limit in limits: print('limit: %-9d precision: %6.2f%% avg time: %.6fs' % (limit, 100.0 * prec_sum[limit] / (i + 1), time_sum[limit] / (i + 1)))
    0 留言 2019-06-24 11:08:26 奖励15点积分
  • 深度学习 21、RNN

    前文链接:https://write-bug.com/article/2644.html
    RNN循环神经网络
    RNN(循环神经网络):一类用于处理序列(时间、空间)数据的神经网络,RNN可用于分类、回归、机器翻译、文本相似度
    隐层的神经元不仅接收输入层的或者上一层的信息流,还要接受上一时刻的同层神经元的信息流,即隐层之间的连接,连接传递的权重就是记忆能力。
    此处的记忆能力理解为人的认知往往基于过往的经验与记忆,所以模拟时,同样根据激活函数的不同会产生梯度消失和梯度爆炸问题,也反映了人的记忆往往有限,过往太久的事不会记忆或者对现在产生影响过大。
    梯度消失:每次loss反向传播时,如果使用sigmoid这样的激活函数一直乘一个小数,在这个过程中一直变小,直到逼近于0梯度爆炸:每次loss反向传播时,如果使用relu这样的激活函数,x>0时,y=x,一直乘一个大于0的数,数值过大
    所以,他的优点就是在上下文推断时,可以根据上文推断下一个出现概率最大的单词,适合短期记忆,不适合长期记忆,很难学习长距离信息,那么同样的,比较符合大脑记忆曲线

    隐层展开后为上图样式。

    t-1, t, t+1表示时间序列
    X表示输入的样本
    St表示样本在时间t处的的记忆,St = f(WSt-1 +UXt)
    W表示输入的权重
    U表示此刻输入的样本的权重

    这里的W,U,V在每个时刻都是相等的(权重共享),隐藏状态: S=f(现有的输入+过去记忆总结)。

    以上,便是最基本的RNN原理,我们为了更好的模拟人们根据过往的经验来判断和写文章,引出了
    LSTM( Long Short Term Memory Network长短期记忆网络)
    LSTM结构更复杂,不仅适合短期记忆,也适合长期记忆。

    这条黑线就是从上一时刻的输入到此刻的输出,即单元状态流,用于记忆当前状态,乘号代表着当前时刻对记忆的削减,加号代表记忆的更新,也就是新信息的汇入。
    LSTM包含3个门 (– 遗忘门 – 输入门 – 输出门)这里有这个“门”的概念,下面一个一个解释:
    遗忘门

    上一刻的信息与此刻的新信息汇入后,σ层就会把信息转化为ft,如果决定要忘记,ft就是0,如果这 个信息要保留,则为1,选择部分记忆则按照实际情况输出0~1的数,也就是激活函数,如sigmoid

    ht-1是上个网络的output(输出)xt当前网络的input(输入)网络会自动学习到对应的权重矩阵
    输入门

    首先,上一刻的信息与此刻的新信息汇入后,,统一经过σ层,决定要更新啥信息。还要另外经过tanh层,将 信息转化为一个新的候选值,两者再相乘,就是cell state中需要更新的值了。


    接着,“遗忘门”和“输入门”已经决定好了 什么要遗忘,什么要更新

    输出门

    前面的门已经决定了信息的状态如何保留和更新,得到了Ct,现在把它经过tanh层,并再次与σ层识别哪一部分信息要输出的信息相乘就得到了输出信息ht,输出两路分别给输出(比如此刻的预测词)和下一时刻输入。
    GRULSTM的变体,比LSTM结构更加简单,效果也很好,GRU只有两个门:更新门、重置门


    zt和rt分别表示更新门和重置门

    更新门用于控制前一时刻的状态信息被带入到当前状态中的程度 ,更新门的值越大说明前一时刻的状态信息带入越多
    重置门控制前一状态有多少信息被写入到当前的候选集 h̃ t 上 ,重置门越小,前一状态的信息被写入的越少

    LSTM 实践# define modelclass RNNModel(nn.Module): """ 一个简单的循环神经网络""" def __init__(self, rnn_type, ntoken, ninp, nhid, nlayers, dropout=0.5): ''' 该模型包含以下几层: - 词嵌入层 - 一个循环神经网络层(RNN, LSTM, GRU) - 一个线性层,从hidden state到输出单词表 - 一个dropout层,用来做regularization ''' super(RNNModel, self).__init__() self.drop = nn.Dropout(dropout) self.encoder = nn.Embedding(ntoken, ninp) if rnn_type in ['LSTM', 'GRU']: self.rnn = getattr(nn, rnn_type)(ninp, nhid, nlayers, dropout=dropout) else: try: nonlinearity = {'RNN_TANH': 'tanh', 'RNN_RELU': 'relu'}[rnn_type] except KeyError: raise ValueError( """An invalid option for `--model` was supplied, options are ['LSTM', 'GRU', 'RNN_TANH' or 'RNN_RELU']""") self.rnn = nn.RNN(ninp, nhid, nlayers, nonlinearity=nonlinearity, dropout=dropout) self.decoder = nn.Linear(nhid, ntoken) self.init_weights() self.rnn_type = rnn_type self.nhid = nhid self.nlayers = nlayers def init_weights(self): initrange = 0.1 self.encoder.weight.data.uniform_(-initrange, initrange) self.decoder.bias.data.zero_() self.decoder.weight.data.uniform_(-initrange, initrange) def forward(self, input, hidden): ''' Forward pass: - word embedding - 输入循环神经网络 - 一个线性层从hidden state转化为输出单词表 ''' #print("input ", input.size()) emb = self.drop(self.encoder(input)) #print("emb ", emb.size()) output, hidden = self.rnn(emb, hidden) #print("output ", output.size()) #print(len(hidden)) output = self.drop(output) decoded = self.decoder(output.view(output.size(0)*output.size(1), output.size(2))) #print("output size: ", output.size()) #print("decoded before size: ", output.view(output.size(0)*output.size(1), output.size(2)).size()) #print("decoded after size: ", decoded.size()) #sys.exit() return decoded.view(output.size(0), output.size(1), decoded.size(1)), hidden def init_hidden(self, bsz, requires_grad=True): weight = next(self.parameters()) if self.rnn_type == 'LSTM': return (weight.new_zeros((self.nlayers, bsz, self.nhid), requires_grad=requires_grad), weight.new_zeros((self.nlayers, bsz, self.nhid), requires_grad=requires_grad)) else: return weight.new_zeros((self.nlayers, bsz, self.nhid), requires_grad=requires_grad)model = RNNModel("LSTM", VOCAB_SIZE, EMBEDDING_SIZE, EMBEDDING_SIZE, 2, dropout=0.5)#模型初始化if USE_CUDA: model = model.cuda()
    0 留言 2019-06-16 20:07:46 奖励15点积分
  • 机器学习 22 、决策树 、模型融合

    前文链接:https://write-bug.com/article/2666.html
    决策树决策树(decision tree)是一种基本的分类与回归方法,模型:树结构
    主要有两大类算法:ID3 、C4.5
    学过数据结构的都知道,一棵树由节点node和有向边directed edge 构成,而节点又有几个名词:根节点、父节点、子节点、叶子节点
    这里我们用各个节点表示特征(如年龄),边表示特征属性(如青年、老年),叶子节点表示label(如买、不买)
    用决策树分类时,来了一个人根据他的特征,对某一特征进行比对,将其分到对应的子节点继续比对,直到到达叶子节点,得到预测label

    特征维度:年龄、收入、信誉、学生
    现在假设每个人都有这四类特征的数据,如何用一颗树来对这些人做一个是否购买的分类,也就是落地到某个叶子节点,那用哪个特征来做根节点父节点子节点呢?有很多组合方式。
    首先处理训练数据:

    数据特征值离散化:0 1 2 ……编码

    我们在进行特征选择时,要选择对训练数据具有分类能力的特征,以提高决策树的学习效率,决定了用哪个特征来划分特征空间,如果利用一个特征进行分类的结果和随即分类的结果没有差别,那么这个特征没有分类能力,而特征选择的准则就是信息增益与信息增益率,从而引出了ID3与C4.5算法
    ID3ID3的特征选择就是利用信息增益来进行选择,这也是我们前面提到的到底用哪个特征作为根节点哪个特征作为子节点构建树呢?
    说到信息增益,这里介绍几个概念:
    熵=随机变量的不确定性
    条件熵=在一定条件下,随机变量的不确定性
    信息增益=熵-条件熵 在一定条件下,信息不确定性减少的程度

    我们通常使用H(X)符号代表随机变量X的熵:
    H(X)=E(I(X))E代表期望值函数,I(X)代表随机变量X的自信息量。

    不确定性函数I称为事件的信息量,事件X发生概率p的单调递减函数:
    𝐼(X) = 𝑙𝑜𝑔(1/ 𝑝)=−𝑙𝑜𝑔 𝑝
    信息熵:一个信源中,不能仅考虑单一事件发生的不确定性,需要考虑所有可能情况的平均不确定性,为−𝑙𝑜𝑔 𝑝 的统计平均值E
    𝐻 (X) = 𝐸 [−𝑙𝑜𝑔 (𝑝 (x𝑖))] = −∑𝑖 𝑝(x𝑖 )𝑙𝑜𝑔 (𝑝 (x𝑖))这里的熵可以理解为混乱度,数据混乱度越大,熵取值越大,随机变量的不确定性越大
    如果H=0或1,说明数据为纯净的,一定为买或不买,而H=0.5时,混乱度最大

    伯努利分布时熵与概率的关系
    选择特征做父节点:选择熵最大(混乱程度最大)的特征
    例:

    ID3 完全用熵的概念:信息增益
    原始数据,无条件下的熵:

    label=买+不买=1024个p买=640/1024=0.635p不买 = 384/1024=0.375根据信息熵公式
    H=-∑p log(p)H=0.9544那么在年龄条件下的条件熵:
    P青年=384/1024=0.375青年分为买和不买:
    p买=128/384p不买=256/384S=384H=0.9183同理:中年:由于都是买的,所以H=0老年:H=0.9157
    年龄的三个信息熵得到了,那如何使用呢,我们对年龄求平均期望:
    平均值=占比\*信息熵E(年龄)=0.375*0.9183+0.25*0+0.375*0.9157=0.6877这个年龄作为区分的话,可以带来0.6877的增益G(年龄)=0.9544-0.6877=0.2667 为信息增益同理

    E(学生)=0.7811 G(学生)=0.1733
    E(收入)=0.9361 G(收入)=0.0183
    E(信誉)=0.9048 G(信誉)=0.0496

    从上述可以看出,按照年龄划分,收益是最大的,信息增益最大,所以年龄作为根节点,同理,从而构建整颗树.
    所以我们构建一个attrList特征列表作为一个容器存放特征,无放回的取特征
    如果取完了特征,构建出了树,但是叶子节点还是不是纯净的怎么办呢?
    分类问题:少数服从多数
    回归问题:概率比较

    缺点:因为优先选择信息增益最大的,所以导致倾向于选择属性值较多的
    贪婪性:选择收益最大的

    贪婪不是最优的算法,可能觉得本次收益最大,但从长远看收益没那么大
    奥卡姆剃刀原理:尽量用最少的东西做最多的事
    用最少的特征,区分类别,不需要所有特征都用,用最简单的方式效果最好,最优的树
    不适用于连续变量:离散型特征属性如果是身高这种线性连续的特征,对应过多类别特征,应离散化使用
    Pseudocode:
    ID3 (Examples, Target_Attribute, Attributes) Create a root node for the tree If all examples are positive, Return the single-node tree Root, with label = +. If all examples are negative, Return the single-node tree Root, with label = -. If number of predicting attributes is empty, then Return the single node tree Root, with label = most common value of the target attribute in the examples. Otherwise Begin A ← The Attribute that best classifies examples. Decision Tree attribute for Root = A. For each possible value, vi, of A, Add a new tree branch below Root, corresponding to the test A = vi. Let Examples(vi) be the subset of examples that have the value vi for A If Examples(vi) is empty Then below this new branch add a leaf node with label = most common target value in the examples Else below this new branch add the subtree ID3 (Examples(vi), Target_Attribute, Attributes – {A}) End Return RootID3实践:

    class ID3DTree(object): def __init__(self): self.tree={} self.dataSet=[] self.labels=[] def loadDataSet(self,path,labels): recordlist = [] fp = open(path,"r") # 读取文件内容 content = fp.read() fp.close() rowlist = content.splitlines() # 按行转换为一维表 recordlist=[row.split("\t") for row in rowlist if row.strip()] self.dataSet = recordlist self.labels = labels def train(self): labels = copy.deepcopy(self.labels) self.tree = self.buildTree(self.dataSet,labels) # 创建决策树主程序 def buildTree(self,dataSet,labels): cateList = [data[-1] for data in dataSet] # 抽取源数据集的决策标签列 # 程序终止条件1 : 如果classList只有一种决策标签,停止划分,返回这个决策标签 if cateList.count(cateList[0]) == len(cateList): return cateList[0] # 程序终止条件2: 如果数据集的第一个决策标签只有一个 返回这个决策标签 if len(dataSet[0]) == 1: return self.maxCate(cateList) # 算法核心: bestFeat = self.getBestFeat(dataSet) # 返回数据集的最优特征轴: bestFeatLabel = labels[bestFeat] tree = {bestFeatLabel:{}} del(labels[bestFeat]) # 抽取最优特征轴的列向量 uniqueVals = set([data[bestFeat] for data in dataSet]) # 去重 for value in uniqueVals: subLabels = labels[:] #将删除后的特征类别集建立子类别集 splitDataset = self.splitDataSet(dataSet, bestFeat, value) # 按最优特征列和值分割数据集 subTree = self.buildTree(splitDataset,subLabels) # 构建子树 tree[bestFeatLabel][value] = subTree return tree def maxCate(self,catelist): # 计算出现最多的类别标签 items = dict([(catelist.count(i), i) for i in catelist]) return items[max(items.keys())] def getBestFeat(self,dataSet): # 计算特征向量维,其中最后一列用于类别标签,因此要减去 numFeatures = len(dataSet[0]) - 1 # 特征向量维数 = 行向量维度-1 baseEntropy = self.computeEntropy(dataSet) # 基础熵:源数据的香农熵 bestInfoGain = 0.0; # 初始化最优的信息增益 bestFeature = -1 # 初始化最优的特征轴 # 外循环:遍历数据集各列,计算最优特征轴 # i 为数据集列索引:取值范围 0~(numFeatures-1) for i in range(numFeatures): # 抽取第i列的列向量 uniqueVals = set([data[i] for data in dataSet]) # 去重:该列的唯一值集 newEntropy = 0.0 # 初始化该列的香农熵 for value in uniqueVals: # 内循环:按列和唯一值计算香农熵 subDataSet = self.splitDataSet(dataSet, i, value) # 按选定列i和唯一值分隔数据集 prob = len(subDataSet)/float(len(dataSet)) newEntropy += prob * self.computeEntropy(subDataSet) infoGain = baseEntropy - newEntropy # 计算最大增益 if (infoGain > bestInfoGain): # 如果信息增益>0; bestInfoGain = infoGain # 用当前信息增益值替代之前的最优增益值 bestFeature = i # 重置最优特征为当前列 return bestFeature def computeEntropy(self,dataSet): # 计算香农熵 datalen = float(len(dataSet)) cateList = [data[-1] for data in dataSet] # 从数据集中得到类别标签 items = dict([(i,cateList.count(i)) for i in cateList]) # 得到类别为key,出现次数value的字典 infoEntropy = 0.0 # 初始化香农熵 for key in items: # 计算香农熵 prob = float(items[key])/datalen infoEntropy -= prob * math.log(prob,2) # 香农熵:= - p*log2(p) --infoEntropy = -prob * log(prob,2) return infoEntropy # 分隔数据集:删除特征轴所在的数据列,返回剩余的数据集 # dataSet:数据集; axis:特征轴; value:特征轴的取值 def splitDataSet(self, dataSet, axis, value): rtnList = [] for featVec in dataSet: if featVec[axis] == value: rFeatVec = featVec[:axis] # list操作 提取0~(axis-1)的元素 rFeatVec.extend(featVec[axis+1:]) # list操作 将特征轴(列)之后的元素加回 rtnList.append(rFeatVec) return rtnList def predict(self,inputTree,featLabels,testVec): # 分类器 root = list(inputTree.keys())[0] # 树根节点 secondDict = inputTree[root] # value-子树结构或分类标签 featIndex = featLabels.index(root) # 根节点在分类标签集中的位置 key = testVec[featIndex] # 测试集数组取值 valueOfFeat = secondDict[key] # if isinstance(valueOfFeat, dict): classLabel = self.predict(valueOfFeat, featLabels, testVec) # 递归分类 else: classLabel = valueOfFeat return classLabel # 存储树到文件 def storeTree(self,inputTree,filename): fw = open(filename,'w') pickle.dump(inputTree,fw) fw.close() # 从文件抓取树 def grabTree(self,filename): fr = open(filename) return pickle.load(fr)C4.5C4.5 用的是信息增益率的概念
    由上面,我们的信息增益:
    𝐺𝑎𝑖𝑛 (𝑆,𝐴)= 𝐸𝑛𝑡𝑟𝑜𝑝𝑦(𝑆) − ∑𝑣∈𝑉𝑎𝑙𝑢𝑒(𝐴) |𝑆𝑣|/ |𝑆| 𝐸𝑛𝑡𝑟𝑜𝑝𝑦 (𝑆𝑣)信息增益率:
    𝐺𝑎𝑖𝑛𝑅𝑎𝑡𝑖𝑜 𝐴 =𝐺𝑎𝑖𝑛(𝐴) / 𝐸𝑛𝑡𝑟𝑜𝑝𝑦(𝐴)比原来多除了一个entropy(A):属性背后的信息熵
    比如:前面的信息增益G(年龄)=0.9544-0.6877=0.2667
    𝐺𝑎𝑖𝑛𝑅𝑎𝑡𝑖𝑜 𝐴=0.2667/0.6877除以一个信息熵有什么好处呢?
    如果只考虑信息增益的话,我们没有考虑A集合背后的大小,A越大也就意味着集合越混乱的可能性越高,那么C4.5考虑到了这种情况,给集合的混乱度做一个中和
    C4.5实践:


    class C45DTree(object): def __init__(self): self.tree={} self.dataSet=[] self.labels=[] def loadDataSet(self,path,labels): recordlist = [] fp = open(path,"r") content = fp.read() fp.close() rowlist = content.splitlines() recordlist=[row.split("\t") for row in rowlist if row.strip()] self.dataSet = recordlist self.labels = labels def train(self): labels = copy.deepcopy(self.labels) self.tree = self.buildTree(self.dataSet,labels) def buildTree(self,dataSet,labels): cateList = [data[-1] for data in dataSet] if cateList.count(cateList[0]) == len(cateList): return cateList[0] if len(dataSet[0]) == 1: return self.maxCate(cateList) bestFeat, featValueList = self.getBestFeat(dataSet) bestFeatLabel = labels[bestFeat] tree = {bestFeatLabel:{}} del(labels[bestFeat]) for value in featValueList: subLabels = labels[:] splitDataset = self.splitDataSet(dataSet, bestFeat, value) subTree = self.buildTree(splitDataset,subLabels) tree[bestFeatLabel][value] = subTree return tree def maxCate(self,catelist): items = dict([(catelist.count(i), i) for i in catelist]) return items[max(items.keys())] def getBestFeat(self, dataSet): Num_Feats = len(dataSet[0][:-1]) totality = len(dataSet) BaseEntropy = self.computeEntropy(dataSet) ConditionEntropy = [] # 初始化条件熵 splitInfo = [] # for C4.5, calculate gain ratio allFeatVList=[] for f in range(Num_Feats): featList = [example[f] for example in dataSet] [splitI,featureValueList] = self.computeSplitInfo(featList) allFeatVList.append(featureValueList) splitInfo.append(splitI) resultGain = 0.0 for value in featureValueList: subSet = self.splitDataSet(dataSet, f, value) appearNum = float(len(subSet)) subEntropy = self.computeEntropy(subSet) resultGain += (appearNum/totality)*subEntropy ConditionEntropy.append(resultGain) # 总条件熵 infoGainArray = BaseEntropy*ones(Num_Feats)-array(ConditionEntropy) infoGainRatio = infoGainArray/array(splitInfo) # c4.5, info gain ratio bestFeatureIndex = argsort(-infoGainRatio)[0] return bestFeatureIndex, allFeatVList[bestFeatureIndex] def computeSplitInfo(self, featureVList): numEntries = len(featureVList) featureVauleSetList = list(set(featureVList)) valueCounts = [featureVList.count(featVec) for featVec in featureVauleSetList] # caclulate shannonEnt pList = [float(item)/numEntries for item in valueCounts ] lList = [item*math.log(item,2) for item in pList] splitInfo = -sum(lList) return splitInfo, featureVauleSetList def computeEntropy(self,dataSet): datalen = float(len(dataSet)) cateList = [data[-1] for data in dataSet] items = dict([(i,cateList.count(i)) for i in cateList]) infoEntropy = 0.0 for key in items: prob = float(items[key])/datalen infoEntropy -= prob * math.log(prob,2) return infoEntropy def splitDataSet(self, dataSet, axis, value): rtnList = [] for featVec in dataSet: if featVec[axis] == value: rFeatVec = featVec[:axis] rFeatVec.extend(featVec[axis+1:]) rtnList.append(rFeatVec) return rtnList # 树的后剪枝 # testData: 测试集 def prune(tree, testData): pass def predict(self,inputTree,featLabels,testVec): root = list(inputTree.keys())[0] secondDict = inputTree[root] featIndex = featLabels.index(root) key = testVec[featIndex] valueOfFeat = secondDict[key] # if isinstance(valueOfFeat, dict): classLabel = self.predict(valueOfFeat, featLabels, testVec) else: classLabel = valueOfFeat return classLabel def storeTree(self,inputTree,filename): fw = open(filename,'w') pickle.dump(inputTree,fw) fw.close() def grabTree(self,filename): fr = open(filename) return pickle.load(fr)之前的ID3和C4.5停止条件都是结点数据类型一致(即纯净),要不就是没有特征可以选择了,但是只有这两个条件限制,在特征多树学习过深的情况下,模型容易过拟合
    防止过拟合:

    停止条件:信息增益(比例)增长低于阈值,则停止
    – 阈值需要调校 将数据分为训练集和测试集
    – 测试集上错误率增长,则停止
    剪枝:

    相邻的叶子节点,如果合并后不纯度增加在允许范围内,则合并
    测试集错误率下降,则合并 等等

    sklearn- tree实践:
    from sklearn.tree import DecisionTreeRegressor# Fit regression modelregr_2 = DecisionTreeRegressor(max_depth=10)#深度阈值print >> sys.stderr, 'begin train ==============='regr_2.fit(X, y)print >> sys.stderr, 'begin predict ==============='# Predicty_2 = regr_2.predict(X_test)for p_label in y_2: print p_label #输出预测标签(回归值)print >> sys.stderr, 'end ==============='多分类用MSE做评估:
    import sysfrom sklearn import metricsimport numpy as npimport randomres_file = sys.argv[1]y_test = []y_pred = []with open(res_file, 'r') as fd: for line in fd: ss = line.strip().split('\t') if len(ss) != 2: continue t_label = float(ss[0].strip()) p_label = float(ss[1].strip()) y_test.append(t_label) y_pred.append(p_label)print "MSE:",metrics.mean_squared_error(y_test, y_pred)print "RMSE:",np.sqrt(metrics.mean_squared_error(y_test, y_pred))模型融合Ensemble Learning(集成学习)什么是集成学习呢?假设我有多个模型,每个模型的学习准确率都是80%,或者说每个模型的准确率有学习好的有学习坏的,这时就有很多数据没有预估正确,那么我使用多个模型来共同决策,少数服从多数,通过这种方式提升准确率,把多个弱分类器集成获得一个强分类器。
    集成学习里面有两类算法:Bagging和Boosting算法
    Bagging算法是以random forest随机森林为代表的算法
    Boosting算法是以AdaBoost算法和Gbdt为代表的算法
    通过介绍我们知道这是将多个模型融合,得到一个强模型的方法,那么我们是如何证明这种方式是否靠谱呢?
    用数学上的排列组合问题表示,假设二分类问题有5个精确度为70%的弱分类器,相互独立
    采用投票的方式将5个分类器的结果进行集成:
    𝐶5 3(0.73)(0.32)+ 𝐶5 4(0.74)(0.31)+ 𝐶5 5(0.71) = 10(.7^3)(.3^2)+5(.7^4)(.3)+(.7^5) = 83.7%如果有101个分类器,精确度就可以提升到99.9%

    问题:如何得到不同的分类器,并且尽量独立? (每个模型不尽相同,不相互影响)
    方法:将一个复杂的学习问题,分解成多个简单的学习问题

    Bagging– 对训练数据采样,得到多个训练集,分别训练模型
    – 对得到的多个模型进行平均 分类问题:投票方式进行集成 回归问题:模型的输出进行平均

    训练:同一训练数据随机抽取数据并行用不同的机器和模型同时训练
    测试:对模型预测出的各个分数进行投票或者平均
    适合弱分类器(容易过拟合,效果一般):
    不稳定:随机采样会得到较不同的分类器,每个基分类器准确率略高于50%
    例如:决策树
    不适合强分类器(本身准确率高,效果较好) :
    稳定:随机采样对结果影响不大
    甚至可能不如不集成,每个基分类器只有更少的训练样本
    例如:DNN(模型复杂时,时间成本大并且容易过拟合,为了抗过拟合,最有效的办法就是让训练数据越大越好,和bagging的选取数据思想不同)
    Random Forest
    Random Forest = Bagging+决策树
    随机森林生成方法:
    – 从样本集中通过重采样的方式产生n个样本
    – 建设样本特征数目为a,对n个样本选择a中的k个特征,用建立决策树的方式获得最佳分割点
    – 重复m次,产生m棵决策树
    – 多数投票机制进行预测
    每棵树样本不同,选择特征可能不同,形成树的形状、深度都不同。
    优点:

    训练速度快、容易实现并行
    训练完成后,反馈哪些是重要特征(类似LR中的特征权重,这里的决策树靠信息熵筛选)
    抗过拟合能力强 (多棵树抗过拟合)
    可解决分类、回归问题
    不平衡数据集,rf是平衡误差有效方法

    缺点:

    取值较多的属性,影响大
    噪音大的分类、回归问题会过拟合
    只能尝试参数和种子的选择,无法洞悉内部

    Boosting每个基分类器,基于之前的分类结果,已经分对的样本不用太多关心,集中力量对付分错样本,不等权投票,好的基分类器权重大
    – 对训练数据集进行采样,对错分样本加权采样
    – 多个模型按照分类效果加权融合

    训练:同一训练数据随机抽取数据串行用不同的机器和模型进行训练,首先模型一学习预估评分后,对出错的样本,提高采样权重,用模型二再次随机抽取数据,这时由于出错样本采样权重提高重点会放在出错的样本上,以此类推串行迭代下去
    测试:对各个模型预测出的各个分数乘以对应的错误率权重公式相加
    Boosting方法的两类思想:
    传统Boost→ Adaboost:对正确、错分样本加权,每步的迭代,错误样本加权,正确样本降权
    Gradient Boosting:每次建模建立在之前模型损失函数的梯度下降方向
    Adaboost
    对不同的模型分数加权求和,错误样本加权,正确样本降权


    给数据中的每一个样本一个权重
    训练数据中的每一个样本,得到第一个分类器
    计算该分类器的错误率,根据错误率计算要给分类器分配的权重(注意这里是分类器的权重) 即前面表示的每个模型预估后的分数乘对应的分类器权重求和

    错误率:𝜀 =未正确分类的样本数目/ 所有样本数目 分类器权重: 𝛼 =1/ 2 ln((1 – 𝜀)/ 𝜀)

    将第一个分类器分错误的样本权重增加,分对的样本权重减小(注意这里是样本的权重)
    错分样本权重:𝐷𝑖^ (𝑡+1) = 𝐷𝑖 ^(𝑡)𝑒^𝑎 /𝑆𝑢𝑚(𝐷),Di^t表示上一次样本的权重,分母:训练集的规模,样本个数
    正确样本权重:𝐷𝑖^ (𝑡+1) = 𝐷𝑖^ (𝑡)𝑒^(−𝑎) / 𝑆𝑢𝑚(𝐷)

    然后再用新的样本权重训练数据,得到新的分类器,到步骤3
    直到步骤3中分类器错误率为0,或者到达迭代次数
    将所有弱分类器加权求和,得到分类结果(注意是分类器权重)

    GBDT(Gradient Boosting Decision Tree)
    是一种迭代的决策树算法,该算法由多棵决策树组,所有树的结论累加起来做最终答案
    三个概念组成:
    – Regression Decistion Tree(DT、RT)
    – Gradient Boosting(GB)
    – Shrinkage(步长)
    分类树:衡量标准最大熵——预测分类标签值
    回归树:衡量标准是最小化均方差——预测实际数值
    GBDT中的树,属于回归树,不是分类树 – 要求所有树的结果进行累加是有意义的 – GBDT是结果累加,而不是多数投票
    GBDT核心: – 每一棵树学的是之前所有树结论和的残差 – 残差就是一个加预测值后能得真实值的累加量
    残差是什么?

    因为是回归树,那么我的节点值应该是这个节点的平均值即预估值,那么来了一个人落在左边这个节点就是15岁,但是对于某些人预估的大了,有的预估的小了,那我把残差整理成新的样本再建一棵树重新学习,尽量把残差学没了,就像以前的误差值,尽力把它学为0
    这里同样的,特征选择时也是利用信息熵的方式。
    残差是学习的最优方向,体现了Gradient
    Gradient Descend梯度下降实际上是在更新参数,并且最终参数等于每次迭代的增量的累加和:
    𝜃𝑡 = 𝜃𝑡−1 + 𝜃𝑡𝜃𝑡 = −𝑎𝑡𝑔𝑡 参数更新方向为负梯度方向𝜃 =∑𝜃𝑡 最终参数等于每次迭代的增量的累加和, 𝜃0为初值而Gradient Boost是在更新整个函数
    𝑓𝑡 𝑥 = 𝑓𝑡−1 𝑥 + 𝑓𝑡 (𝑥)𝑓𝑡(𝑥) = −𝑎𝑡𝑔𝑡(𝑥) 类似地,拟合了负梯度𝐹(𝑥) = ∑𝑓𝑡(𝑥) 最终参数等于每次迭代的增量的累加和 𝑓0(𝑥)为模型初值,通常为函数所以,Boosting是一种加法模型GBDT模型F定义为加法模型
    𝐹 (𝑥;𝑤) = ∑𝑎𝑡 ℎ𝑡(𝑥;𝑤𝑡)= 𝑓𝑡(𝑥;𝑤𝑡)x为输入样本,h为分类回归树,w是分类回归树的参数,a是每棵树的权重

    2.1的公式是loss值更新函数预估值,2.2是求残差
    但是这样的加法模式比较容易过拟合,这就涉及到了Shrinkage缩减技术:
    思想:每棵树只学到了真理的一小部分,累加的时候只累加一小部分,通过多学几棵树弥补不足
    假设: yi表示第i棵树上y的预测值, y(1~i)表示前i棵树y的综合预测值
    没有shrinkage时:
    – y(i+1) = 残差(y1~yi), 其中: 残差(y1~yi) = y真实值 - y(1 ~ i)
    – y(1 ~ i) = SUM(y1, …, yi)
    有shrinkage时:
    – 第2个方程调整为:y(1 ~ i) = y(1 ~ i-1) + step /* yi
    即对每次的结果都乘一个权重
    仍然以残差作为学习目标,但对于残差的学习,每次只累加一小部分(step残差)逐步逼近目标
    本质上,Shrinkage为每棵树设置了一个weight,累加时要乘以这个weight
    偏差、方差
    偏差bias:模型不准确
    尽量选择正确模型与复杂度
    模型越简单,预估能力越差,偏差越大,方差越低
    方差variance:模型不稳定
    防止过拟合

    我们的模型融合是可以同时把方差与偏差同时降低的方法。
    RF实践:
    sklearn - rf:
    测试数据:

    import sysimport numpy as npimport matplotlib.pyplot as pltimport pandas as pd# Importing the datasetsdatasets = pd.read_csv('Position_Salaries.csv')#pandas自动解析读取X = datasets.iloc[:, 1:2].values #级别Y = datasets.iloc[:, 2].values #金额# Fitting the Regression model to the datasetfrom sklearn.ensemble import RandomForestRegressorregressor = RandomForestRegressor(n_estimators = 300, random_state = 0)#300颗树regressor.fit(X,Y)# Predicting a new result with the Random Forest RegressionY_Pred = regressor.predict([[6.5]])print(Y_Pred)# Visualising the Random Forest Regression results in higher resolution and smoother curveX_Grid = np.arange(min(X), max(X), 0.01)X_Grid = X_Grid.reshape((len(X_Grid), 1))plt.scatter(X,Y, color = 'red')plt.plot(X_Grid, regressor.predict(X_Grid), color = 'blue')plt.title('Random Forest Regression Results')plt.xlabel('Position level')plt.ylabel('Salary')plt.show()output:

    pyspark-mllib-rf:
    测试数据:

    #coding=utf8from __future__ import print_functionimport sysfrom pyspark import SparkContext, SparkConffrom pyspark.mllib.regression import LabeledPointfrom pyspark.mllib.feature import HashingTF,IDF,StandardScalerfrom pyspark.mllib.classification import LogisticRegressionWithSGD,SVMWithSGD,NaiveBayesfrom pyspark.mllib.tree import DecisionTreefrom pyspark.mllib.tree import RandomForest, RandomForestModelfrom pyspark.mllib.util import MLUtilsreload(sys)sys.setdefaultencoding('utf-8')if __name__ == "__main__": #conf = SparkConf().setMaster("spark://master:7077").setAppName("lr_pyspark_test") conf = SparkConf().setMaster("local").setAppName("lr_pyspark_test") sc = SparkContext(conf=conf) #in_file = sc.textFile("file:///root/spark_test_5/pyspark_test/lr_pyspark_test/data/a8a") data = MLUtils.loadLibSVMFile(sc, "file:///root/spark_test_5/pyspark_test/lr_pyspark_test/data/a8a") (trainingData, testData) = data.randomSplit([0.7, 0.3]) model = RandomForest.trainClassifier(\ trainingData, numClasses=2, \ categoricalFeaturesInfo={},\ numTrees=3, \ featureSubsetStrategy="auto",\ impurity='gini', maxDepth=4, maxBins=32)#3颗树,gini系数方式 predictions = model.predict(testData.map(lambda x: x.features)) labelsAndPredictions = testData.map(lambda lp: lp.label).zip(predictions) testErr = labelsAndPredictions.filter(lambda (v, p): v != p).count() / float(testData.count()) print('Test Error = ' + str(testErr)) print('Learned classification forest model:') print(model.toDebugString()) ## Save and load model #model.save(sc,"target/tmp/myRandomForestClassificationModel") #sameModel = RandomForestModel.load(sc, "target/tmp/myRandomForestClassificationModel") sc.stop()GBDT实践:
    微软lightgbm方式:
    import sysimport lightgbm as lgb #pip安装import numpy as npfrom scipy.sparse import csr_matriximport pandas as pdfrom sklearn.metrics import mean_squared_errorfrom sklearn.datasets import load_irisfrom sklearn.model_selection import train_test_splitfrom sklearn.datasets import make_classificationdata_in = 'D:\\bd\\share_folder\\a9a'def load_data(): target_list = [] fea_row_list = [] fea_col_list = [] data_list = [] row_idx = 0 max_col = 0 with open(data_in, 'r') as fd: for line in fd: ss = line.strip().split(' ') label = ss[0] fea = ss[1:] target_list.append(int(label)) for fea_score in fea: sss = fea_score.strip().split(':') if len(sss) != 2: continue feature, score = sss fea_row_list.append(row_idx) fea_col_list.append(int(feature)) data_list.append(float(score)) if int(feature) > max_col: max_col = int(feature) row_idx += 1 row = np.array(fea_row_list) col = np.array(fea_col_list) data = np.array(data_list) fea_datasets = csr_matrix((data, (row, col)), shape=(row_idx, max_col + 1)) x_train, x_test, y_train, y_test = train_test_split(fea_datasets, target_list, test_size=0.2, random_state=0) return x_train, x_test, y_train, y_testX_train,X_test,y_train,y_test = load_data()# 创建成lgb特征的数据集格式lgb_train = lgb.Dataset(X_train, y_train)lgb_eval = lgb.Dataset(X_test, y_test, reference=lgb_train)params = { 'task': 'train', 'boosting_type': 'gbdt', # 设置提升类型 'objective': 'regression', # 目标函数 'metric': {'l2', 'auc'}, # 评估函数 'num_leaves': 31, # 叶子节点数 'learning_rate': 0.05, # 学习速率 'feature_fraction': 0.9, # 建树的特征选择比例 'bagging_fraction': 0.8, # 建树的样本采样比例 'bagging_freq': 5, # k 意味着每 k 次迭代执行bagging 'verbose': 1 # <0 显示致命的, =0 显示错误 (警告), >0 显示信息}print('Start training...')# 训练 cv and traingbm = lgb.train(params,lgb_train,num_boost_round=20,valid_sets=lgb_eval,early_stopping_rounds=5)print('Save model...')# 保存模型到文件gbm.save_model('model.txt')print('Start predicting...')# 预测数据集y_pred = gbm.predict(X_test, num_iteration=gbm.best_iteration)print(y_test)print(y_pred)# 评估模型print('The rmse of prediction is:', mean_squared_error(y_test, y_pred) ** 0.5)部分模型output:
    0 留言 2019-06-21 08:22:26 奖励12点积分
  • 机器学习 14、分类算法-NB、AUC

    前文链接:https://write-bug.com/article/2428.html
    分类算法更新:NB、PR/AUC/ROC、LR、Softmax、DT、RF、GDBT、DNN、CNN、RNN
    什么是分类问题呢?
    最常见的机器学习任务:给定一个对象X,将其划分到预定义好的某一个类别Yi。
    比如说:性别识别,人群划分,新闻分类,query分类,商品分类,网页分类,垃圾邮件过滤,网页排序等等。
    对于性别识别来说,分辨男女,也就是二分类问题,即判断0和1问题。
    那我们如何评判一个人是男是女呢?这时就需要特征来解决:肤色、身高、体重、音色等等等等,我们人类打眼一看什么特征大概就知道这个人是男是女了,那机器是如何学会这个技能的呢?
    我们可以把这个机器当成是一个幼儿园小孩,如果幼儿园老师告诉小朋友,0:老虎很凶猛,1:小猫很温顺,那这时来了个狮子,小孩大脑里就会自动的拿狮子的特征去比对所见过的东西,觉得有些像老虎,那就会自然而然地离它远一些了。
    这个技能就是泛化能力,也就是说来了一个新东西,如果我能预测的很准的话,那就说明这次机器学习的模型是效果很好的,是具有泛化能力的。
    如果我想对文章做分类,一篇文章可以有很多类别,即多分类,那其中的特征是什么呢?关键词token,我提取出一篇文章的关键词,很容易就可以看出这篇文章属于哪类,每个词都有着对每个类别的倾向性,比如说‘股市’大概率都是财经文章。
    机器学习模型
    我们人类可以通过自己的学习经验一眼就可以把分类问题做出来,如果有大量数据的时候我们就需要机器去判断,也就是机器学习。
    机器学习目的:模型,可以帮我们对数据做预测。
    如果我想要机器学习对新数据预测的分类准确率高的话,我们就需要两点:

    好模型(老师/算法)
    好数据(教材/训练集)

    * 好模型 ≠ 复杂模型:

    复杂模型和性能开销成正比,即实现程度
    过拟合,对训练集学习太过深刻会把训练集中的错误数据知识也学习到了,缺少泛化能力,对测试预估效果影响较大

    评估模型学习效果:PR/AUC/ROC
    朴素贝叶斯(NB)算法
    我们在中文分词那节的语言模型中,推导过贝叶斯定理:
    p(s|c)=p(c|s)p(s)/p(c)推导:
    p(s|c) p(c)=p(s,c)—联合概率(同时发生的概率)->p(c|s)p(s)=p(c,s)-> p(s|c) p(c)= p(c|s)p(s)-> p(s|c)= p(c|s)p(s)/p(c)那假如我们设定:
    X:一篇文章
    Y:文章类别集合{}
    yi:第i个类别
    xi:文章某token
    p(yi|X):一篇文章属于哪个类别的概率
    则有:p(yi|X)=p(X|yi)p(yi)/p(X)
    p(yi):先验概率,即常识,已知规律
    我们看上面的设定来说,p(yi)代表着这个类别的概率,也就是说假设我训练集有100篇文章,我提前知道军事类别50,财经类别30,体育类别20,那么我们的先验概率也就固定下来了,p(y=军事)=50/100,以此类推,那么等到我测试集进行预测时,我的先验概率也按照这个比例来分布。
    p(X):常数,一篇文章出现的概率就像我们原先说的一个人说的每句话一样几率都是一样的。
    所以,公式简化为:
    p(yi|X)=p(X|yi)p(yi)p(X|yi)=p(x1|y1)\*p(x2|y1)\*p(x3|y1)....也就是把大X分成不同的关键词,但前提是独立同分布。
    所以朴素贝叶斯的前提:独立同分布
    那这里我需要对p(yi|X)=p(X|yi)p(yi)的参数进行估计:即最大似然估计
    p(yi),如何计算呢?统计,说白了就是我们刚才举例子的方法求得的。
    p(X|yi),两种计算方法:
    p(xi|yi)=count(xi,yi)/count(yi)count(xi,yi):即特征(关键词)xi和类别yi在训练数据中同时出现的次数
    count(yi):即某类别总数
    p(谷歌|军事)=军事类文章中包含‘谷歌’这个词的文章个数 / 军事类文章个数p(谷歌|军事)=军事类文章中包含‘谷歌’这个词的文章个数 / 军事类文章中所有词个数通过上面的分析与推导,我们得到了两个参数,如果来了一篇文章我们去预测属于那个类别,即p(yi|X),让这两个参数相乘就ok了。这个结果是一个概率,这就意味着如果是二分类问题,p(y=0|X)=70%,p(y=1|X)=30%,即有多少分类就要求多少个概率值,取概率最大的数,最致信。这个结果就是后验概率。
    优点:– 简单有效 – 结果是概率,对二值和多值同样适用
    缺点:– 独立性假设有时不合理
    NB实践:
    1.python DataConvert.py data/ nb_data
    数据准备:分好词的几千篇文章
    数据合并和转换:标签和token
    数据编码:token编码为数字,分测试集和训练集
    2.python NB.py 1 nb_data.train model
    训练数据训练,得到model(前面的两个参数)
    3.python NB.py 0 nb_data.test model out
    测试数据用model作预测—》求log
    二分类评估效果
    那我们怎么去评估我们的分类效果呢?对于二分类来说,我们最初使用混淆表(混淆矩阵)的方式进行评测:


    准确度Accuracy:(C11+C22)/(C11+C12+C21+C22)
    精确率Precision(y1):C11/(C11+C21)
    召回率Recall(y1):C11/(C11+C12)

    这种方式不好看,我们来替换成例子来看下:


    准确度Accuracy:(50+35)/(35+5+10+50)=85%
    精确率Precision(y1):50/(50+5)=90.9%
    召回率Recall(y1):50/(50+10)=83.3%

    上面的例子很明确,通常在工作中一般注重这两个指标,正确率(精确率)和召回率:

    正确率:预测样本中,正确的样本所占的比例,即看军事列
    召回率:预测样本中,正确的样本占同一类别总的样本比例,看军事行

    那么什么指标合适,在日常生活中,有的是侧重于召回,有的是侧重于正确率,越靠近底层一般越侧重于召回,越往上,越侧重于精确,即Nosq库那块侧重召回,排序模型那里侧重于精确
    上面我们提出了混淆矩阵,得到了准确率和召回率:
    这里引出了:PR、ROC、AUC
    有了这个正确率和召回率,我们可以获得一个PR曲线,即:同时评估正确率和召回率的方法就是通过PR曲线,p代表正确率,R代表召回率但是这个PR曲线很容构造成一个高正确率或高召回率的曲线,很难保证两全齐美,一般准确率高,召回率就低,召回率高,准确率低,可以构成一个二维码坐标,纵坐标是正确率,横坐标是召回率,用来确定阈值thd

    那么用生活中的两类场景来举例子:
    搜索场景,保证召回的前提下,再提高准确率
    疾病检测,保证准确性前提,提升召回
    PR曲线是个评测指标,用来协助你去选择阀值的,这个阈值怎么理解呢?比如我几篇文章被预测为军事的概率分别是0.7、0.8、0.6,如果阈值设置为thd=0.5,那么这三篇文章全被分辨为正例,都是军事。通过对阈值的调参,我得到的混淆矩阵里面的数值不同。
    那么我们看下ROC曲线的评测指标:

    纵轴:真阳率,召回率,TP/(TP+FN)
    横轴:假阳率FP/(FP+FN)
    那么ROC曲线有什么用,其实ROC曲线是为了得到AUC,即ROC曲线下的面积,,不同的阈值生成不同的混淆矩阵数值,打一个点就得到下面的曲线

    但是这样计算比较麻烦,我们可以利用其他方式去理解AUC,即负样本排在正样本前面的概率。
    假如A 0.1 B 0.9 我们假设负样本排正样本前面的概率认为正确,即A在B前面,认为是一次正确,B排在A前面,认为是一次错误。也就是按照概率进行倒序排序,上面是最小的概率,那么理想状态下,所有负例应该排在正例前面才对。
    我们可以通过一个AWK来计算:
    cat auc.raw | sort -t$'\t' -k2g |awk -F'\t' '($1==-1){++x;a+=y;}($1==1){++y;}END{print 1.0-a/(x\*y);}'x*y是正负样本pair对,a代表错误的个数,a/x*y 错误的概率,1-a/x*y 正确概率
    解释一下这个linux命令,按照第二个模型打的分数进行循环判断,小的排在前面,大的分数排在后面,当有的分数是比较小,但是不是该类的,这个a就加一个y,y从0开始加,直到结束,能够找到有多少a,进而计算评估的正确率
    例如:来了个文章,我们假如是军事类为+1,财经为-1,当然这个文章是军事类文章,即+1,然后我们设置一个阀值为0,即分类预测的分数 >0 认为是+1,<=0 认为是-1,x是负样本的个数,y是所有正样本个数,a是错误的样本个数

    那么这个最差就是0.5,压根不知道好坏,评测是到底是正确的评测还是错误的评测。最完美就是1或者0了
    0 留言 2019-04-28 12:40:13 奖励13点积分
  • 机器学习 15、分类算法 -LR

    前文链接:https://write-bug.com/article/2439.html
    逻辑回归前节中,我们分析了什么是分类以及机器学习的模型介绍、NB算法的应用、和二分类模型的评估。我们在这里再次总结一下机器学习的要素(括号内是对于LR而言):

    机器学习用数据训练出模型再应用的过程
    训练集训练所用的输入与输出
    测试集评估所用的输入与输出
    监督学习机器学习中,分为监督学习和非监督式学习,而监督式学习就是数据集有着明确的答案(即label),可供后续寻找输入输出之间的关系(误差)
    模型描述输入输出之间关系的方式(等式参数)
    训练学习输入输出之间关系的过程
    预测解决输入的预期输出的过程
    评估衡量模型好坏的过程
    泛化能力对一个前所未见的输入可以给出一个正确的预测

    在谈LR之前,我们需要介绍一些铺垫知识:
    一元线性回归何为回归为题?假如有一二维坐标系,横坐标x:西瓜重量,纵坐标y:西瓜价格,上面散落着一些点,即每个西瓜重量都有着对应的价格。如下图:

    那我如何根据这些数据,获取到一些规律,每次随意给我一个西瓜重量我都可以预测一个合理价格出来呢?我们需要像图中绿色的那条线一样,尽力去拟合所有数据,即 y=wx+b,给定x西瓜重量,返回y西瓜价格,未知参数:w,b。
    在上图中我们可以看到很多data点,同样的也就可以画出很多条线,那我们如何才能找到最可以拟合所有数据的最优线呢?我们可以给每个点和每条线之间都求取点到线的距离,即线没有拟合点的误差值,那么当我们找最优线的时候,我们把每次和这条线求的所有误差值加起来,看谁最小就好了,即最小化误差平方和:
    ∑((wx+b) - y)^2即loss function 找出最优参数,获取最优线。
    Error = y - y^那么如何求取最优参数?因为lossfunc为二次方程,坐标系中为下凸函数,所以求导=0就可以得出极小值:最优参数w,b

    模型输出:y^=wx+b
    模型参数:w,b

    多元线性回归在一元中,我们输入x为一个单因素值,即标量。在多元中,我们输入的x为多因素值,即向量。那么什么情况为多因素值呢?比如还是西瓜,那我的西瓜价格不只是重量影响着,还有颜色,拍的响声等等,而这些因素就是西瓜的特征,而西瓜在这里成为样本。
    但是这种多维度向量并不能在二维坐标系中显示,每个样本都有着多维度特征,可以想象,像二维坐标系中显示一样,在高纬度空间中散落着这些点,如果要做回归问题,就需要有一个超平面拟合这些数据点,如果做分类问题则需要一个超平面去尽力把两类样本点分开,而这里的西瓜问题是个回归问题。而这个超平面也就意味着我们的参数也变成了多维向量。即:
    X[x1,x2,x3...xn]W[w1,w2,w3,...wn]wx向量相乘是求内积。
    之前我们的等式y=wx+b,在机器学习中有了些新的名称:

    X:样本特征向量
    W:权重向量
    B:偏置

    比如:

    X样本为一个西瓜,那么西瓜的特征可能有:重量、颜色、响声、把的位置等等
    W权重即这些特征的重要程度,每个特征的权重都影响着最后的score
    B偏置的意思就是比如说各个地区的西瓜长得样子普遍不一样,那对于这类样本来说给他一个惩罚值平衡样本数据

    同样的,我们得到多元回归方程:
    𝑓(𝑥𝑖,𝑤,𝑏) = ∑𝑤𝑗𝑥𝑖𝑗 + 𝑏 = [𝑥𝑖1 ⋯ 𝑥𝑖𝑘][𝑤1 ⋮ 𝑤𝑘]+ 𝑏 = 𝑤𝑖𝑥 + 𝑏我们把b约去得到:
    𝑓(𝑥𝑖,𝑤,𝑏) = [𝑥𝑖1 ⋯ 𝑥𝑖𝑛][𝑤1 ⋮ 𝑤𝑛]+ 𝑏 = [𝑥𝑖1 ⋯ 𝑥𝑖𝑛 1][𝑤1 ⋮ 𝑤𝑛 𝑏]= 𝑤 · 𝑥𝑖所以只需要求:
    𝑓(𝑥𝑖,𝑤) = 𝑤𝑥和前面一样,为了求取最优参数,需要最小化误差平方和:
    min 𝑤 𝐿(𝑤) = ∑ (𝑤𝑥𝑖 – 𝑦𝑖)^2------》e只不过单因素标量变成了多因素向量了而已。
    同样,求导=0,但是这里w也是个向量, 根据微积分公式需要对每个w特征权重求偏导,得出:
    W=(XTX)^-1 XTYXT为X向量的转置,Y为输出后的向量
    当XT X 不可逆时,设定正数λ,W=(XTX + λI)^-1 XTY
    逻辑回归逻辑回归,logistic回归,是一种广义的线性回归分析模型,常用于数据挖掘,疾病自动诊断,经济预测等领域。常用于二分类问题,而多分类问题常用SoftMax函数来解决。
    上节说过,二分类问题也就是01问题,比如判断一个人是男是女,可以从多个特征来分辨:肤色、身高、体重、音色等等,每个特征都有自己的权重,像前面一样代表各个特征的影响度。
    设定1为男性,0为女性,则P(y=1|x)代表男性的概率,P(y=0|x)代表女性的概率。通常情况下,我们研究正例的概率,也就是y=1的概率,那么有:
    P(y=1|x)=f(x)这里的输入和前面的多元线性回归一样,但是输出是一个score,即
    f(x)=wx但是这里的score也就是y是一条直线,值域是(-∞,+∞),而概率的值域是[0-1],那如何让P(y=1|x)=wx呢?一起来推导下:
    exp(wx)的值域是[0,+无穷)p(y=0|x) = 1 - p(y=1|x)p(y=1|x) / (1 - p(y=1|x))的值域是[0,+无穷)p(y=1|x) / (1 - p(y=1|x))= exp(wx)p(y=1|x) = 1/(exp(-wx) +1)以上,我们通过压缩变换值域把他强制变到一个0-1的概率值
    在实际应用中,是客观存在这种非线性变换函数的,这里LR用Sigmoid函数:

    可以发现,和我们上面的想法推导几乎一致,但是这个函数不是我们推导出来的,而是客观存在的函数:

    那么这里我们求w这个最优参数值呢?
    在NB朴素贝叶斯中求取概率值,我们运用最大似然估计参数从而求得概率值。
    那么在这里我们同样对P运用最大似然:
    P(Y|X)=P^y * P^(1-y)这么表示并无实际含义,只是当y=0或1时的概率,那我们对它使用极大似然估计参数w:
    MLE w = arg max log(P(Y|X)) = arg max Π P(yi|xi) = arg max ∑ log P(yi|xi) = arg max ∑(yi LogP(y=1|X) +(1-yi) LogP(y=0|X))p(y=1|x) = 1/(exp(-wx) +1)p(y=0|x) = 1 - p(y=1|x)所以有:
    L(w)=-∑(yi log(𝜂(wx)) +(1-yi) (1-log(𝜂(wx))))为什么前面有个减号呢?最大似然函数是求取参数的极大值,是上凸函数,前面加个符号,图像就变成了下凸函数,这样我们就可以求取到参数极小值。
    看到上面的式子,如果你熟悉信息熵的话,是可以直接得到上面的式子的,即
    MLEmax = lossfunction min (- min CrossEntropy)信息熵公式:
    H(x)= - ∑ P(xi)log(p(xi))后面我们会有地方介绍信息熵,我们得到上面的L(w)负对数似然公式,由于它是高阶连续可导下凸函数,此时就可以将他作为损失函数来求解:
    利用梯度下降法更新参数w,求导得:
    ▽L(w)= -∑(yi- 𝜂(wx))xi机器学习的损失函数是人为设计的,用于评判模型好坏(对未知的预测能力)的一个标准,就像去评判任何一件事物一样,从不同角度看往往存在不同的评判标准,不同的标准往往各有优劣,并不冲突。唯一需要注意的就是最好选一个容易测量的标准,不然就难以评判了。
    其次,既然不同标准并不冲突,那使用最小二乘作为逻辑回归的损失函数当然是可以,那这里为什么不用最小二乘而用最大似然呢?请看一下最小二乘作为损失函数的函数曲线
    J(θ)=∑(yi- 𝜂(θx))
    以及最大似然作为损失函数的函数曲线
    L(w)=-∑(yi log(𝜂(wx)) +(1-yi) (1-log(𝜂(wx))))
    很显然,图2比图1展现的函数要简单多,很容易求到参数的最优解(凸函数),而图1很容易陷入局部最优解(非凸函数)。这就是前面说的选取的标准要容易测量,这就是逻辑回归损失函数为什么使用最大似然而不用最小二乘的原因了。
    此时,我们就可以和前面一样通过lossfunc求取最优参数w,通过不断降低差距来使预测值接近真实值(即反向传递的误差):
    由于L(w)是高阶连续可导的凸函数,根据凸优化理论,可利用梯度下降法、 牛顿法等求解,求导得到梯度:
    ▽L(w)= -∑(yi- 𝜂(wx))xi梯度:大小为导数的值,方向指向误差上升最快的方向
    当参数w维度大于1时,用相同的方法对各个维度求e偏导数(e(w)’),偏导数所组成的向量就是梯度。
    导数和梯度的区别是:导数是变化率,而梯度是一个和参数维度一样的向量。
    各个维度的数值等于对应的偏导数,那么让参数向梯度相反的方向更新,从而减小误差值。所以,使用梯度下降法更新最小化F(W),从而更新参数W:

    设置初始化w,计算F(w)=sigmoid(wx ) ->预估值我们的数据前面说过都是有label值的,也就是真实值,得到预测值和真实值之间的误差,我们希望通过学习数据让误差越来越小。
    计算梯度
    ▽F(w)=▽L(w)= -∑(yi- 𝜂(wx))xi此时我们已经得到了y真实值,w初始化值,x样本特征值,可以发现,梯度式子里面就是一个误差公式,那我们通过计算就可以直接代入乘以样本向量,得到梯度。下降方向
    dir = -▽F(w)负号:前面说过,反方向是下降最快方向
    尝试梯度更新
    𝑤𝑛𝑒𝑤 = 𝑤 +步长∗ 𝑑𝑖𝑟 得到下降后的 𝑤𝑛𝑒𝑤和F(w𝑛𝑒𝑤)如果 F(w𝑛𝑒𝑤)- F(w)较小,停止; 否则 ;跳到第2步
    1 留言 2019-05-08 22:26:17 奖励21点积分
  • 机器学习 16、分类算法 -Softmax

    前文链接:https://write-bug.com/article/2467.html
    梯度下降BGD
    在前文中,我们通过梯度下降法最优化更新参数w,那么上节那种迭代的方法为BGD,批量梯度下降。这种方法每次都把所有数据集都遍历一遍,从而每次都可以更新最优的参数。这种方式比较慢,实用性差但是效果好。

    SGD
    正常工作中,我们通常使用SGD,随机梯度下降,此种方法快,使用当前数据集的最优参数通过多次迭代不断逼近全局最优值。

    MBGD
    工业界中,常用方法minibatch小批量梯度下降,把所有样本分为不同的批次依次执行BGD(参数不变一直更新同一参数),效果介于前两种之间的折中方式。


    高参:不是模型参数,又影响训练结果的因素,根据不同的任务,人工调节
    Learning rate:参数以多大的幅度利用梯度更新,即走的步长epoch:遍历所有样本次数Batch size :每个batch中包含多少个样本的个数
    Batch:批次,每次epoch中,包含多个batch
    Step:更新一次批量
    shuffle优化:每次取的样本做随机打乱

    更新过程:
    计算误差值,将误差作为参数计算梯度,将所有参数减去对应梯度乘以学习速率完成一次更新
    多分类Softmax逻辑回归是Softmax的一般形式。在二分类中我们求取p(y=1|x)=sigmoid(wx)的概率作为正例,同时只求一组w,b的权重就可以了。
    但是在多分类中,我们的分类集合为{1,2,3,4。。。。。k}应用:数字识别、文章分类等
    那么这时,我们就需要对这个样本属于每个分类都求取一组权重:
    P(y=1|x)—w1=e^(w1x)P(y=2|x)—w2=e^(w2x)P(y=3|x)—w3=e^(w3x)P(y=1|x)=w1/(w1+w2+w3)所以求取到的每个概率看那个概率最大就属于哪个类。
    在给定输入x,对每一个类别j估算出概率值为p(y=j|x)
    输出一个k维向量(元素和为1)来表示这k个估计的概率值

    向量中所有概率相加=1。
    我们知道,逻辑回归的损失函数是

    那么由此我们可推广出softmax回归损失函数:


    k:当前类别,m:真实label
    只不过是softmax对k个值进行累加,logic对2个值进行累加
    同样的,对损失函数求导得:

    这时,你可能会好奇,到底怎么logic就成了softmax的特殊情况了,若k=2,Softmax回归退化为logistic回归:

    两个参数向量均减去向量θ1:

    此时,就得到了两个参数组成的向量,此值就是原来的概率值。
    我们在上面得到了梯度公式,就可以通过每个类乘学习率,各自更新自己的权重,得到一列权重向量。
    L1、L2正则我们可以看到上面提出的三种梯度公式都有一个λw,而这个λw可加可不加,加了之后相当于每次迭代更新时都变相给各自的权重加了个约束,对于大部分训练,效果都会变好,即正则化项。

    λ:衰退权重
    常用正则化项:

    L1范数:λ|w| 又称lassoL2范数:λ|w|^2 又称ridge(岭回归)当然在训练中,L1和L2是可以加起来跑的。L1+L2:(梯度+L1+L2)

    在日常使用中,我们需要对所有维度做一个综合考虑,达到效果最好。假设分辨男女的特征,类似戴眼镜这种没有区分能力的特征我们需要把他的w学为0,还有如果我的w声音学到了1000,w身高学到了100,w体重学到了10,单纯看每一个特征它的w都比较合理,但是从整体来看,w小的特征就会被强特征淹没掉,比如前面这几个只要出现了声音特征可能就一定确认为男性。这样的效果并不是我们想要的,泛化能力弱。
    这个正则项就是对w的惩罚项,如果加了正则项就相当于压缩了w:
    L1:|w|<1L2:sqrt(|w|^2)<1W是一组向量在公式中与各自的X相乘作内积,那上面压缩了w就相当于对每个w乘一个相同小数,Wold和Wnew比例关系相同,那么和之前的wx就是一回事了。


    由上图,我们把每次w更新的值想象为等高线(效果相同),在同倍数缩小时,和L1,L2构成的图形相交(每次更新的w都和我们的λ有着直接联系,看如何选择最优那组w)达到限制w扩散过大的目的。同时从图形中我们可以看出一些特点:

    L1:w1+w2=1,某时w与其角上会重合,过小的w会被优化为0,形成稀疏矩阵
    从系统实现角度:大大减少参数存储空间(只存储有数部分)、针对性强,泛化能力好,防止过拟合
    L2: w1^2+w2^2=1,与圆相交相切几乎都不为0,不会产生稀疏矩阵,可以对一票否决权的这种权重惩罚的也大,让权值尽可能小,防止过拟合

    欠拟合、过拟合在大部分算法中,都存在过拟合问题,即一个模型在训练集上表现得非常好,但是在实际测试中效果差,将噪声一起学到了模型中

    上图中,既可以表现过拟合的现象,又是一种防止过拟合的方法:即交叉验证
    上面的线为测试集错误率,下面为训练集错误率,在训练的同时进行测试(每训练几轮测试一下),一旦测试集错误率上升,立即停止训练,防止过拟合。
    那么还有什么防止方法呢?

    筛选特征(过多特征的筛选、降维)
    人工筛选:根据时间、重要程度等等对特征进行优先级排列或筛选
    机器筛选(如mrMR信息增益(-决策树)):比如一个特征对男女判断是否有用,可以对其求auc

    特定模型方法
    增加惩罚项(L1、L2)
    决策树高度

    增加训练数据(万能)
    神经网络Dropout(减边)

    欠拟合:一般都是训练轮数不够,那么一般增加训练轮数和决策树拓展分支
    1 留言 2019-05-16 13:39:17 奖励16点积分
  • 机器学习 18、聚类算法-Kmeans

    前文链接:https://write-bug.com/article/2530.html
    上节 我们暂时实现了一下单机版推荐系统,并且串了下知识,这节介绍下聚类,说起聚类就得先提下监督和非监督式学习:

    监督式学习:我们之前学习的分类、回归问题中的排序模型LR、Softmax,包括后面的dnn,dt等等都是需要数据事先有一个标签label作为新特征的分类
    非监督式学习:而这个有些类似之前的推荐算法,并没有特定的类别作为比对

    而今天学习的算法聚类算法也是非监督式学习。
    聚类是什么?之前对于文本分类来说,需要事先设定好类别标签对新文本进行概率分类、对label对比,而这里的聚类则可以既不需要设定类别,也不需要事先设定文本中心,其可自动将数据划分到不同的类中,使相似的数据划分到相同的类中,这种方法可应用到几乎所有分类中,用户聚类、商品聚类、生物特征、基因、图像聚类等等分类。
    比如说,随机系统中有10000篇文章,我只告诉系统分为多少个类,其他什么都不告诉,这个系统就可以自动操作完成。
    事实上,就是对特征的向量化及空间划分。
    向量化如果想对一个文章、物品等作聚类,那聚类之前,如何表示文章呢?
    向量
    比如之前的tfidf,用一个向量来表示物品之后作cos、内积、计算质心等等
    质心:各个向量的点构成的空间的中心,即每个点到这个点的最短平均距离
    A:(a1, a2, a3)B:(b1, b2, b3)C:(c1, c2, c3)质心=( (a1+b1+c1)/3, (a2+b2+c2/3, (a3+b3+c3)/3)距离矩阵、相似度矩阵
    此矩阵就像之前的推荐算法中的矩阵,各个pair相似度的矩阵
    评估如何评估聚类效果?
    内部方法
    同类尽可能相似,不同类尽可能不相似

    分子为对象到质心距离和,越小越好
    分母为质心距离,越大越好
    外部方法(准确P、召回R)
    之前利用pr曲线找到最好的分类面,但是需要有监督的情况下才能用,这里如果想用,在特定情况下,比如说文章有固定标签,用这种方法训练后再告诉label评估效果,但是工作中几乎遇到的都是无标签的,所以这种方法是很鸡肋的方法,工作中基本用内部方法解决。
    F=PR /(P+R)聚类类别层次聚类


    自底向上凝聚聚类,从不同的对象凝聚结果形成聚合节点,成树状
    自顶向下分裂聚类,从一个对象分裂结果形成分裂节点,成树状

    优点:

    可通过阈值灵活控制类个数
    层次用于概念聚类,分类后人工对其打标签,抽象概念。

    凝聚实现:

    将每个对象样本分为一类,得到N个类
    找到最接近的两个类合并为一个类:两层for循环遍历所有样本,计算cos相似度
    重新计算新类与旧类之间距离:类别之间距离和类别与对象之间距离的计算方式一般使用组合链的方式进行计算:即类中质心距离(平均距离),而与之相对的有单链(最近距离)和全链(最远距离)两种方式,一般工作中就直接使用组合链方式
    重复直到最后合并为一个类
    复杂度O(n^3)

    非层次聚类:Kmeans
    层次聚类是一种需要将向量两两比较聚类的方法,但这种方法的计算时间过长,影响效率,而Kmeans是随机选择对象作为类别中心,然后优化这些中心点的位置,使尽可能收敛到和真实中心一致的算法。
    步骤:

    任意选择K个点作为初始聚类中心 (初始化类个数)
    根据每个聚类的中心,计算每个对象与这些中心的距离(cos、欧式距离),并根据 最小距离重新对相应对象进行划分

    欧式距离:A:(a1, a2, a3) B:(b1, b2, b3)距离=sqrt((a1-b1)^2 + (a2-b2)^2 + (a3-b3)^2 )
    重新计算每个聚类的中心 (更新质心计算)
    当满足一定条件,如类别划分不再发生变化时,算法终止,否则 继续步骤2和3
    复杂度:O(Ktn^2)K个点,n个对象,t次循环,n次计算质心

    之前介绍时为什么说是向量化并分割空间,下面这个图每个点都是空间中的向量点,把两个中心点相连,中心分割的垂线就是分割面,再迭代更新质心位置,从而分割空间


    一定条件:

    轮数
    WCSS损失函数:最小化簇内对象到质心的距离平方和

    wcss变小,即空间变化不剧烈

    缺点:过于依赖初始点选择,可能导致的结果大不一样
    解决方法:只能多训练几轮,求质心平均值或者选择最小的wcss的模型。
    模型:最终计算结果的K个中心点向量
    K选择,业务场景:
    举例:半个性化推荐,即热榜推荐列表只有一部分是个性化对用户的推荐(如视频网站首页上半部),但是当用户上亿,但热榜或首页展示目录只有10个,候选集合只有100个的时候怎么办?如何给大量的用户也做一个模糊的推荐?如何在100个候选集合中选择10个作为推荐列表?
    对上亿用户作聚类,可以随意聚几千个类,再用这几千个类的向量和每个物品向量作cos,选出分数前10的物品向量作为此用户类的候选集合。
    用户向量化:利用历史行为作用户画像:token作向量
    层次 vs Kmeans

    从上面两张图,我们可以看到右面Kmeans算法的暴力分割和缺点,在生活中的数据大部分都是球面原始数据,也是Kmeans的强项,左面的那种长条数据只能用层次聚类分辨出来,而与此同时左面五颜六色的单点都是噪音数据,前面说过层次聚类非常耗时,并且我们事先并不知道数据的分布,所以部分解决办法就是增加K类个数。
    Kmeans粒度比较粗,层次粒度比较细。
    聚类这种发现规律的分类效果永远无法赶上分类回归那种有监督式的效果,即有老师无老师的区别。如果效果不好,可以适当增加K个数增加准确率。
    0 留言 2019-05-29 15:55:53 奖励16点积分
显示 15 到 30 ,共 15 条

热门回复

eject